1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

..

63 Commits

Author SHA1 Message Date
Laurent Cozic
21f9189000 Server v2.13.2 2023-10-19 20:50:02 +01:00
github-actions[bot]
d92032e634 @PiotrNarel has signed the CLA in laurent22/joplin#9095 2023-10-19 17:56:16 +00:00
Laurent Cozic
5986710fc0 Server: Significantly improve sync performances, especially when there are many changes 2023-10-19 17:46:58 +01:00
Laurent Cozic
4d1e0cc21b Chore: Server: Added test tools to automatically populate the database (#9085) 2023-10-19 17:11:20 +01:00
renovate[bot]
7b42211581 Update dependency @types/yargs to v17.0.26 (#9092)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-19 03:34:29 +00:00
renovate[bot]
2fc7bcec06 Update dependency react-native-webview to v13.6.0 (#9089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 12:38:13 +01:00
renovate[bot]
3e3d01d93e Update dependency tap to v16.3.9 (#9087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 06:14:03 +00:00
renovate[bot]
f634a1c731 Update dependency @types/react-redux to v7.1.27 (#9079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 03:42:05 +00:00
renovate[bot]
1fe91b4808 Update dependency @types/react-dom to v18.2.8 (#9078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 02:29:39 +00:00
renovate[bot]
d20c48855c Update dependency @types/markdown-it to v13.0.2 (#9082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 02:01:17 +00:00
renovate[bot]
38d310c0ad Update dependency @react-native-community/geolocation to v3.1.0 (#9088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 22:22:39 +01:00
pedr
c06ca87573 Chore: Export extractNoteFromHTML from notes services library (#9086) 2023-10-17 21:10:18 +01:00
renovate[bot]
d50d940f3c Update dependency @types/mustache to v4.2.3 (#9083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 16:26:04 +00:00
Joplin Bot
bde74d1f97 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-17 12:22:02 +00:00
Laurent Cozic
11f7915a54 Doc: Fixed typo 2023-10-17 11:24:57 +01:00
renovate[bot]
4501ecff3b Update dependency react-native-image-picker to v5.7.0 (#9077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 09:50:25 +01:00
renovate[bot]
387ba2c50f Update dependency markdown-it to v13.0.2 (#9073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-16 21:20:44 +00:00
renovate[bot]
7e085ef0bc Update dependency @testing-library/react-native to v12.3.0 (#9074)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-16 21:39:21 +01:00
renovate[bot]
cfb1f11956 Update dependency @types/react to v18.2.23 (#9071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-16 19:18:49 +00:00
renovate[bot]
6668a52478 Update dependency @types/styled-components to v5.1.28 (#9067)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 21:40:24 +00:00
renovate[bot]
92230fce72 Update dependency react-native-zip-archive to v6.1.0 (#9068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 20:04:26 +01:00
Laurent Cozic
f2269e9820 Chore: Fixed node-glob issues 2023-10-15 18:51:37 +01:00
Laurent Cozic
fbef66b65a Tools: Add node-glob to Renovate ignore list 2023-10-15 13:52:13 +01:00
renovate[bot]
8795da83d2 Update dependency @types/yargs to v17.0.25 (#9064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 10:46:21 +00:00
renovate[bot]
79f9b93b58 Update dependency tar to v6.2.0 (#9065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 10:28:52 +01:00
Helmut K. C. Tessarek
5fd4c19b8d All: Translation: Update da_DK.po (thanks ERYpTION) 2023-10-14 05:58:13 -04:00
renovate[bot]
7621dde8e7 Update dependency @types/nodemailer to v6.4.11 (#9062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-14 09:16:58 +00:00
renovate[bot]
02a797abe9 Update dependency @types/node-rsa to v1.1.2 (#9061)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-14 06:30:24 +00:00
renovate[bot]
41d4734bd3 Update dependency @types/node-fetch to v2.6.6 (#9060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-14 03:50:59 +00:00
renovate[bot]
0c91e2c947 Update dependency @types/node to v18.17.19 (#9059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-14 02:03:04 +00:00
Henry Heino
2d06fd9d13 Chore: Desktop: Set up integration testing with Playwright (#9043) 2023-10-13 17:32:10 +03:00
pedr
5733017637 Chore: Move useful clipper logic to the lib package to be used in other places (#9053) 2023-10-13 17:31:13 +03:00
renovate[bot]
b1e1db7831 Update dependency glob to v10.3.6 (#9057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-13 03:21:27 +00:00
pedr
c5c03ab04e Chore: Improve isNode to be more inclusive (#9054) 2023-10-12 15:27:48 +03:00
Laurent Cozic
5cecfde085 Update translations 2023-10-12 15:26:55 +03:00
Joplin Bot
0c701f59c7 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-12 12:23:15 +00:00
Joplin Bot
05a4affd5a Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-12 06:21:15 +00:00
renovate[bot]
84ff840c15 Update dependency react-native-share to v9.4.1 (#9052)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 15:43:00 +00:00
Laurent Cozic
4989d402a4 Fix config 2023-10-11 16:13:19 +03:00
Laurent Cozic
3ac25104c3 Merge branch 'dev' of github.com:laurent22/joplin into dev 2023-10-11 13:05:30 +03:00
renovate[bot]
2a73010c9d Update dependency react-native-share to v9.4.0 (#9050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 12:46:20 +03:00
Henry Heino
0f005c5039 Desktop: Fixes #8881: Fix markdown editor context menu not displaying on some devices (#9030) 2023-10-11 10:18:32 +01:00
Laurent Cozic
0402fa624d All: Support for plural translations (#9033) 2023-10-11 10:17:46 +01:00
renovate[bot]
a98c323bf3 Update dependency react-select to v5.7.5 (#9048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 09:16:28 +00:00
Laurent Cozic
d975d8d626 Tools: Auto-apply Electron minor and patch updates 2023-10-11 10:15:22 +01:00
renovate[bot]
19c694760f Update dependency react-native-paper to v5.10.6 (#9047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 07:25:54 +00:00
renovate[bot]
9236f68016 Update dependency glob to v10.3.5 (#9046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 04:20:09 +00:00
renovate[bot]
a052983fb6 Update dependency @types/react to v18.2.22 (#9044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 16:43:35 +00:00
renovate[bot]
089c6afd1a Update react monorepo (#9032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 13:59:23 +03:00
renovate[bot]
4b7f807b5b Update dependency react-native-webview to v13.5.1 (#9029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 13:59:03 +03:00
renovate[bot]
d4ccf06f98 Update dependency @types/node to v18.17.18 (#9042)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 07:34:31 +00:00
renovate[bot]
2d9980dd36 Update dependency follow-redirects to v1.15.3 (#9041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 00:47:17 +00:00
renovate[bot]
db4e08b757 Update dependency dayjs to v1.11.10 (#9040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-09 18:44:28 +00:00
renovate[bot]
10cef6e146 Update dependency react-native-share to v9.3.0 (#9038)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-09 19:40:55 +03:00
renovate[bot]
d33df54969 Update dependency sharp to v0.32.6 (#9035)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-09 04:10:07 +00:00
renovate[bot]
90feb65b44 Update dependency react-native-paper to v5.10.5 (#9034)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-09 01:21:47 +00:00
Laurent Cozic
cbe260eed2 Chore: Convert build-translations to TS 2023-10-08 19:20:53 +01:00
renovate[bot]
2f7801a267 Update dependency @react-native-community/datetimepicker to v7.5.0 (#9031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-08 18:57:07 +01:00
renovate[bot]
d58f62ca9d Update contributor-assistant/github-action action to v2.3.1 (#9025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-08 01:57:38 +00:00
renovate[bot]
d6f272c74f Update dependency react-native-device-info to v10.9.0 (#9026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-07 22:23:08 +01:00
Laurent Cozic
cd55a9a40f Mobile: Fix sidebar folder icon 2023-10-07 19:48:38 +01:00
Laurent Cozic
5cb54a57ac Android 2.13.2 2023-10-07 18:09:36 +01:00
Laurent Cozic
0e0c1d8395 Mobile: Fix icon after react-native-vector-icon upgrade 2023-10-07 17:25:03 +01:00
167 changed files with 152482 additions and 53743 deletions

View File

@@ -38,6 +38,10 @@ packages/app-clipper/popup/config/webpack.config.js
packages/app-clipper/popup/node_modules
packages/app-clipper/popup/scripts/build.js
packages/app-desktop/build/
packages/app-desktop/test-results/
packages/app-desktop/playwright-report/
packages/app-desktop/playwright/.cache/
packages/app-desktop/integration-tests/test-profile/
packages/app-desktop/dist
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js
@@ -371,6 +375,13 @@ packages/app-desktop/gui/style/StyledTextInput.js
packages/app-desktop/gui/utils/NoteListUtils.js
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/integration-tests/main.spec.js
packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/bridge.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js
@@ -575,6 +586,7 @@ packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
packages/lib/commands/index.js
@@ -955,6 +967,7 @@ packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/tools/build-release-stats.js
packages/tools/build-translation.js
packages/tools/build-welcome.js
packages/tools/buildServerDocker.test.js
packages/tools/buildServerDocker.js

View File

@@ -13,7 +13,7 @@ jobs:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: contributor-assistant/github-action@v2.3.0
uses: contributor-assistant/github-action@v2.3.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret

View File

@@ -55,6 +55,9 @@ jobs:
sudo apt-get install -y libsecret-1-dev
sudo apt-get install -y translate-toolkit
sudo apt-get install -y rsync
# Provides a virtual display on Linux. Used for Playwright integration
# testing.
sudo apt-get install -y xvfb
- name: Install Docker Engine
# if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')

9
.gitignore vendored
View File

@@ -357,6 +357,13 @@ packages/app-desktop/gui/style/StyledTextInput.js
packages/app-desktop/gui/utils/NoteListUtils.js
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/integration-tests/main.spec.js
packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/bridge.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js
@@ -561,6 +568,7 @@ packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
packages/lib/commands/index.js
@@ -941,6 +949,7 @@ packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/tools/build-release-stats.js
packages/tools/build-translation.js
packages/tools/build-welcome.js
packages/tools/buildServerDocker.test.js
packages/tools/buildServerDocker.js

View File

@@ -9,7 +9,7 @@
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-share-permissions.png" alt=""></p>
<h2>Email to Note<a name="email-to-note" href="#email-to-note" class="heading-anchor">🔗</a></h2>
<p>Joplin Cloud Pro and Teams also now include the Email to Note feature, allowing you to conveniently store your emails within Joplin Cloud. By simply forwarding your emails to your Joplin Cloud address, you can transform them into notes. The email's subject will serve as the note title, while the body of the email will be the note's content. These notes will be organized within a notebook named &quot;Inbox.&quot;</p>
<p>More information in the <a href="https://joplinapp.org/email%5C_to%5C_note/">Email to Note documentation</a>.</p>
<p>More information in the <a href="https://joplinapp.org/email_to_note/">Email to Note documentation</a>.</p>
<h2>Choose to resize an image or not<a name="choose-to-resize-an-image-or-not" href="#choose-to-resize-an-image-or-not" class="heading-anchor">🔗</a></h2>
<p>By default, when you add a large image, Joplin will ask you if you would like to shrink it down or not. With this new release, you now have the option to always ask, to always resize, or to never resize the image, giving you more flexibility and reducing the number of prompts in the app.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-resize-note.png" alt=""></p>

View File

@@ -537,47 +537,47 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 77%
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 22%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 56%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 44%
<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) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 86%
<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) | 98%
<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) | Fejby | 96%
<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) | ERYpTION | 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) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 98%
<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) | | 43%
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 84%
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 21%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 54%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 42%
<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) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 92%
<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) | 97%
<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) | Fejby | 94%
<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) | ERYpTION | 97%
<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) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 97%
<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) | | 42%
<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%
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 25%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 93%
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 24%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 94%
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 100%
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 28%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 86%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 78%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 75%
<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) | | 76%
<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) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 86%
<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) | [Mats Estensen](mailto:code@mxe.no) | 85%
<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) | 53%
<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) | [X3NO](mailto:X3NO@disroot.org) | 88%
<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) | [Douglas Leão](mailto:djlsplays@gmail.com) | 85%
<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) | 70%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 49%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 78%
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 27%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 84%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 76%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 73%
<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) | | 74%
<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) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 97%
<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) | [Mats Estensen](mailto:code@mxe.no) | 83%
<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) | 52%
<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) | [X3NO](mailto:X3NO@disroot.org) | 85%
<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) | [Fernando Nagase](mailto:nagase.fernando@gmail.com) | 97%
<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) | 68%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 48%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 76%
<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) | 97%
<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) | | 35%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 75%
<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) | | 34%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 73%
<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) | 97%
<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) | 98%
<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) | 97%
<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) | [Dmitriy K](mailto:dmitry@atsip.ru) | 97%
<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) | | 63%
<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) | [qx100](mailto:ztymaxwell@gmail.com) | 98%
<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) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 86%
<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) | 88%
<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) | 86%
<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) | 95%
<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) | 94%
<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) | [Титан](mailto:fignin@ya.ru) | 96%
<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) | | 61%
<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) | [qx100](mailto:ztymaxwell@gmail.com) | 97%
<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) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 84%
<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) | 97%
<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) | 84%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Contributors

View File

@@ -79,7 +79,7 @@
"eslint-plugin-react": "7.33.2",
"execa": "5.1.1",
"fs-extra": "11.1.1",
"glob": "10.3.4",
"glob": "10.3.10",
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",

View File

@@ -57,7 +57,7 @@
"proper-lockfile": "4.1.2",
"read-chunk": "2.1.0",
"server-destroy": "1.0.1",
"sharp": "0.32.5",
"sharp": "0.32.6",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
@@ -73,7 +73,7 @@
"@joplin/tools": "~2.13",
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
"@types/node": "18.17.17",
"@types/node": "18.17.19",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.6.4",

View File

@@ -0,0 +1,114 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.getStyleSheets = exports.getImageSizes = void 0;
function absoluteUrl(url) {
if (!url) { return url; }
const protocol = url.toLowerCase().split(':')[0];
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) { return url; }
if (url.indexOf('//') === 0) {
return location.protocol + url;
} else if (url[0] === '/') {
return `${location.protocol}//${location.host}${url}`;
} else {
return `${baseUrl()}/${url}`;
}
}
function pageLocationOrigin() {
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
// in this case.
if (location.protocol === 'file:') {
return 'file://';
} else {
return location.origin;
}
}
function baseUrl() {
let output = pageLocationOrigin() + location.pathname;
if (output[output.length - 1] !== '/') {
const output2 = output.split('/');
output2.pop();
output = output2.join('/');
}
return output;
}
function getJoplinClipperSvgClassName(svg) {
for (const className of svg.classList) {
if (className.indexOf('joplin-clipper-svg-') === 0) { return className; }
}
return '';
}
function getImageSizes(element, forceAbsoluteUrls = false) {
const output = {};
const images = element.getElementsByTagName('img');
for (let i = 0; i < images.length; i++) {
const img = images[i];
if (img.classList && img.classList.contains('joplin-clipper-hidden')) { continue; }
let src = imageSrc(img);
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
if (!output[src]) { output[src] = []; }
output[src].push({
width: img.width,
height: img.height,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
});
}
const svgs = element.getElementsByTagName('svg');
for (let i = 0; i < svgs.length; i++) {
const svg = svgs[i];
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) { continue; }
const className = getJoplinClipperSvgClassName(svg); // 'joplin-clipper-svg-' + i;
if (!className) {
console.warn('SVG without a Joplin class:', svg);
continue;
}
if (!svg.classList.contains(className)) {
svg.classList.add(className);
}
const rect = svg.getBoundingClientRect();
if (!output[className]) { output[className] = []; }
output[className].push({
width: rect.width,
height: rect.height,
});
}
return output;
}
exports.getImageSizes = getImageSizes;
// In general we should use currentSrc because that's the image that's currently displayed,
// especially within <picture> tags or with srcset. In these cases there can be multiple
// sources and the best one is probably the one being displayed, thus currentSrc.
function imageSrc(image) {
if (image.currentSrc) { return image.currentSrc; }
return image.src;
}
// Given a document, return a <style> tag that contains all the styles
// required to render the page. Not currently used but could be as an
// option to clip pages as HTML.
// eslint-disable-next-line
function getStyleSheets(doc) {
const output = [];
for (let i = 0; i < doc.styleSheets.length; i++) {
const sheet = doc.styleSheets[i];
try {
for (const cssRule of sheet.cssRules) {
output.push({ type: 'text', value: cssRule.cssText });
}
} catch (error) {
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
// by the desktop application, since it doesn't have CORS restrictions.
// eslint-disable-next-line
console.info('Could not retrieve stylesheet now:', sheet.href);
// eslint-disable-next-line
console.info('It will downloaded by the main application.');
// eslint-disable-next-line
console.info(error);
output.push({ type: 'url', value: sheet.href });
}
}
return output;
}
exports.getStyleSheets = getStyleSheets;
// # sourceMappingURL=clipperUtils.js.map

View File

@@ -20,20 +20,6 @@
browserSupportsPromises_ = false;
}
function absoluteUrl(url) {
if (!url) return url;
const protocol = url.toLowerCase().split(':')[0];
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
if (url.indexOf('//') === 0) {
return location.protocol + url;
} else if (url[0] === '/') {
return `${location.protocol}//${location.host}${url}`;
} else {
return `${baseUrl()}/${url}`;
}
}
function escapeHtml(s) {
return s
.replace(/&/g, '&amp;')
@@ -49,85 +35,6 @@
return document.title.trim();
}
function pageLocationOrigin() {
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
// in this case.
if (location.protocol === 'file:') {
return 'file://';
} else {
return location.origin;
}
}
function baseUrl() {
let output = pageLocationOrigin() + location.pathname;
if (output[output.length - 1] !== '/') {
output = output.split('/');
output.pop();
output = output.join('/');
}
return output;
}
function getJoplinClipperSvgClassName(svg) {
for (const className of svg.classList) {
if (className.indexOf('joplin-clipper-svg-') === 0) return className;
}
return '';
}
function getImageSizes(element, forceAbsoluteUrls = false) {
const output = {};
const images = element.getElementsByTagName('img');
for (let i = 0; i < images.length; i++) {
const img = images[i];
if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
let src = imageSrc(img);
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
if (!output[src]) output[src] = [];
output[src].push({
width: img.width,
height: img.height,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
});
}
const svgs = element.getElementsByTagName('svg');
for (let i = 0; i < svgs.length; i++) {
const svg = svgs[i];
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) continue;
const className = getJoplinClipperSvgClassName(svg);// 'joplin-clipper-svg-' + i;
if (!className) {
console.warn('SVG without a Joplin class:', svg);
continue;
}
if (!svg.classList.contains(className)) {
svg.classList.add(className);
}
const rect = svg.getBoundingClientRect();
if (!output[className]) output[className] = [];
output[className].push({
width: rect.width,
height: rect.height,
});
}
return output;
}
function getAnchorNames(element) {
const output = [];
// Anchor names are normally in A tags but can be in SPAN too
@@ -146,14 +53,6 @@
return output;
}
// In general we should use currentSrc because that's the image that's currently displayed,
// especially within <picture> tags or with srcset. In these cases there can be multiple
// sources and the best one is probably the one being displayed, thus currentSrc.
function imageSrc(image) {
if (image.currentSrc) return image.currentSrc;
return image.src;
}
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
// And hard-code the image dimensions so that the information can be used by the clipper server to
// display them at the right sizes in the notes.
@@ -181,6 +80,7 @@
}
if (nodeName === 'img') {
// eslint-disable-next-line no-undef
const src = absoluteUrl(imageSrc(node));
node.setAttribute('src', src);
if (!(src in imageIndexes)) imageIndexes[src] = 0;
@@ -199,6 +99,7 @@
}
if (nodeName === 'svg') {
// eslint-disable-next-line no-undef
const className = getJoplinClipperSvgClassName(node);
if (!(className in imageIndexes)) imageIndexes[className] = 0;
@@ -216,11 +117,13 @@
}
if (nodeName === 'embed') {
// eslint-disable-next-line no-undef
const src = absoluteUrl(node.src);
node.setAttribute('src', src);
}
if (nodeName === 'object') {
// eslint-disable-next-line no-undef
const data = absoluteUrl(node.data);
node.setAttribute('data', data);
}
@@ -300,6 +203,7 @@
let svgId = 0;
for (const svg of svgs) {
// eslint-disable-next-line no-undef
if (!getJoplinClipperSvgClassName(svg)) {
svg.classList.add(`joplin-clipper-svg-${svgId}`);
svgId++;
@@ -307,30 +211,6 @@
}
}
// Given a document, return a <style> tag that contains all the styles
// required to render the page. Not currently used but could be as an
// option to clip pages as HTML.
function getStyleSheets(doc) {
const output = [];
for (let i = 0; i < doc.styleSheets.length; i++) {
const sheet = doc.styleSheets[i];
try {
for (const cssRule of sheet.cssRules) {
output.push({ type: 'text', value: cssRule.cssText });
}
} catch (error) {
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
// by the desktop application, since it doesn't have CORS restrictions.
console.info('Could not retrieve stylesheet now:', sheet.href);
console.info('It will downloaded by the main application.');
console.info(error);
output.push({ type: 'url', value: sheet.href });
}
}
return output;
}
function documentForReadability() {
// Readability directly change the passed document so clone it so as
// to preserve the original web page.
@@ -372,7 +252,9 @@
name: shouldSendToJoplin ? 'sendContentToJoplin' : 'clippedContent',
title: title,
html: html,
// eslint-disable-next-line no-undef
base_url: baseUrl(),
// eslint-disable-next-line no-undef
url: pageLocationOrigin() + location.pathname + location.search,
parent_id: command.parent_id,
tags: command.tags || '',
@@ -397,6 +279,7 @@
response.warning = 'Could not retrieve simplified version of page - full page has been saved instead.';
return response;
}
// eslint-disable-next-line no-undef
return clippedContentResponse(article.title, article.body, getImageSizes(document), getAnchorNames(document));
} else if (command.name === 'isProbablyReaderable') {
@@ -408,6 +291,7 @@
} else if (command.name === 'completePageHtml') {
if (isPagePdf()) {
// eslint-disable-next-line no-undef
return clippedContentResponse(pageTitle(), embedPageUrl(), getImageSizes(document), getAnchorNames(document));
}
@@ -417,10 +301,12 @@
// Because cleanUpElement is going to modify the DOM and remove elements we don't want to work
// directly on the document, so we make a copy of it first.
const cleanDocument = document.body.cloneNode(true);
// eslint-disable-next-line no-undef
const imageSizes = getImageSizes(document, true);
const imageIndexes = {};
cleanUpElement(convertToMarkup, cleanDocument, imageSizes, imageIndexes);
// eslint-disable-next-line no-undef
const stylesheets = convertToMarkup === 'html' ? getStyleSheets(document) : null;
// The <BODY> tag may have a style in the CSS stylesheets. This
@@ -462,9 +348,11 @@
container.appendChild(range.cloneContents());
}
// eslint-disable-next-line no-undef
const imageSizes = getImageSizes(document, true);
const imageIndexes = {};
cleanUpElement(convertToMarkup, container, imageSizes, imageIndexes);
// eslint-disable-next-line no-undef
return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document));
} else if (command.name === 'screenshot') {
@@ -567,6 +455,7 @@
const content = {
title: pageTitle(),
crop_rect: selectionArea,
// eslint-disable-next-line no-undef
url: pageLocationOrigin() + location.pathname + location.search,
parent_id: command.parent_id,
tags: command.tags,
@@ -591,7 +480,9 @@
} else if (command.name === 'pageUrl') {
// eslint-disable-next-line no-undef
const url = pageLocationOrigin() + location.pathname + location.search;
// eslint-disable-next-line no-undef
return clippedContentResponse(pageTitle(), url, getImageSizes(document), getAnchorNames(document));
} else {

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "2.13.0",
"version": "2.13.1",
"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

@@ -1,6 +1,8 @@
const fs = require('fs-extra');
const sourcePath = `${__dirname}/../../lib/randomClipperPort.js`;
const clipperUtilsPath = `${__dirname}/../../lib/clipperUtils.js`;
// Mozilla insists on building the clipper from a tarball, not from the repository
// so we add this check and only copy the file if it's present. Normally it rarely
@@ -10,6 +12,10 @@ if (fs.pathExistsSync(sourcePath)) {
fs.copySync(sourcePath, `${__dirname}/src/randomClipperPort.js`);
}
if (fs.pathExistsSync(clipperUtilsPath)) {
fs.copySync(clipperUtilsPath, `${__dirname}/../content_scripts/clipperUtils.js`);
}
// These files give warnings when loading the extension in Chrome, in dev mode
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_key.pem`);
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_rsa_pubkey.pem`);

View File

@@ -182,6 +182,7 @@ class AppComponent extends Component {
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/clipperUtils.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
}

View File

@@ -14,3 +14,7 @@ style.min.css
build/lib/
vendor/*
!vendor/loadEmojiLib.js
test-results/
playwright-report/
playwright/.cache/
integration-tests/test-profile/

View File

@@ -260,7 +260,7 @@ const EncryptionConfigScreen = (props: Props) => {
<br/>
<MacOSMissingPasswordHelpLink
theme={theme}
text={_('%s: Missing password', _('Help'))}
text={_('%s: Missing password.', _('Help'))}
/>
</p>
);

View File

@@ -2,17 +2,16 @@
import { ContextMenuEvent, ContextMenuParams } from 'electron';
import { useEffect, RefObject } from 'react';
import { _ } from '@joplin/lib/locale';
import Setting from '@joplin/lib/models/Setting';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import convertToScreenCoordinates from '../../../../utils/convertToScreenCoordinates';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import eventManager from '@joplin/lib/eventManager';
import bridge from '../../../../../services/bridge';
import Setting from '@joplin/lib/models/Setting';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@@ -35,7 +34,7 @@ const useContextMenu = (props: ContextMenuProps) => {
// It might be buggy, refer to the below issue
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
useEffect(() => {
const isAncestorOfCodeMirrorEditor = (elem: HTMLElement) => {
const isAncestorOfCodeMirrorEditor = (elem: Element) => {
for (; elem.parentElement; elem = elem.parentElement) {
if (elem.classList.contains(props.editorClassName)) {
return true;
@@ -45,14 +44,9 @@ const useContextMenu = (props: ContextMenuProps) => {
return false;
};
let lastInCodeMirrorContextMenuTimestamp = 0;
// The browser's contextmenu event provides additional information about the
// target of the event, not provided by the Electron context-menu event.
const onBrowserContextMenu = (event: Event) => {
if (isAncestorOfCodeMirrorEditor(event.target as HTMLElement)) {
lastInCodeMirrorContextMenuTimestamp = Date.now();
}
const convertFromScreenCoordinates = (zoomPercent: number, screenXY: number) => {
const zoomFraction = zoomPercent / 100;
return screenXY / zoomFraction;
};
function pointerInsideEditor(params: ContextMenuParams) {
@@ -64,13 +58,15 @@ const useContextMenu = (props: ContextMenuProps) => {
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
if (!elements.length || !isEditable) return false;
const maximumMsSinceBrowserEvent = 100;
if (Date.now() - lastInCodeMirrorContextMenuTimestamp > maximumMsSinceBrowserEvent) {
return false;
}
const rect = convertToScreenCoordinates(Setting.value('windowContentZoomFactor'), elements[0].getBoundingClientRect());
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
// Checks whether the element the pointer clicked on is inside the editor.
// This logic will need to be changed if the editor is eventually wrapped
// in an iframe, as elementFromPoint will return the iframe container (and not
// a child of the editor).
const zoom = Setting.value('windowContentZoomFactor');
const xScreen = convertFromScreenCoordinates(zoom, x);
const yScreen = convertFromScreenCoordinates(zoom, y);
const intersectingElement = document.elementFromPoint(xScreen, yScreen);
return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement);
}
async function onContextMenu(event: ContextMenuEvent, params: ContextMenuParams) {
@@ -160,11 +156,8 @@ const useContextMenu = (props: ContextMenuProps) => {
// the listener that shows the default menu.
bridge().window().webContents.prependListener('context-menu', onContextMenu);
window.addEventListener('contextmenu', onBrowserContextMenu);
return () => {
bridge().window().webContents.off('context-menu', onContextMenu);
window.removeEventListener('contextmenu', onBrowserContextMenu);
};
}, [
props.plugins, props.editorClassName, editorRef,

View File

@@ -0,0 +1,17 @@
# Integration tests
The integration tests in this directory can be run with `yarn playwright test`.
- Tests use a `test-profile` directory that should be re-created before every test.
- Only one Electron application should be instantiated per test file.
- Files in the `models/` directory follow [the page object model](https://playwright.dev/docs/pom).
# References
The following sources are helpful for designing and implementing Electron integration tests
with Playwright:
- [A setup guide from an organisation that uses Playwright](https://dev.to/kubeshop/testing-electron-apps-with-playwright-3f89)
and [that organisation's test suite](https://github.com/kubeshop/monokle/blob/main/tests/base.test.ts).
- [The Playwright ElectronApp docs](https://playwright.dev/docs/api/class-electronapplication)
- [Electron Playwright example repository](https://github.com/spaceagetv/electron-playwright-example)
- [Playwright best practices](https://playwright.dev/docs/best-practices)

View File

@@ -0,0 +1,79 @@
import { test, expect } from './util/test';
import MainScreen from './models/MainScreen';
import activateMainMenuItem from './util/activateMainMenuItem';
import SettingsScreen from './models/SettingsScreen';
test.describe('main', () => {
test('app should launch', async ({ mainWindow }) => {
// A window should open with the correct title
expect(await mainWindow.title()).toMatch(/^Joplin/);
const mainPage = new MainScreen(mainWindow);
await mainPage.waitFor();
});
test('should be able to create and edit a new note', async ({ mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.newNoteButton.click();
const editor = mainScreen.noteEditor;
await editor.waitFor();
// Wait for the title input to have the correct placeholder
await mainWindow.locator('input[placeholder^="Creating new note"]').waitFor();
// Fill the title
await editor.noteTitleInput.click();
await editor.noteTitleInput.fill('Test note');
// Note list should contain the new note
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
// Focus the editor
await editor.codeMirrorEditor.click();
// Type some text
await mainWindow.keyboard.type('# Test note!');
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.type('New note content!');
// Should render
const viewerFrame = editor.getNoteViewerIframe();
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
});
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.waitFor();
// Sort order buttons should be visible by default
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).toBeVisible();
// Open settings (check both labels so that this works on MacOS)
expect(
await activateMainMenuItem(electronApp, 'Preferences...') || await activateMainMenuItem(electronApp, 'Options'),
).toBe(true);
// Should be on the settings screen
const settingsScreen = new SettingsScreen(mainWindow);
await settingsScreen.waitFor();
// Open the appearance tab
await settingsScreen.appearanceTabButton.click();
// Find the sort order visible checkbox
const sortOrderVisibleCheckbox = mainWindow.getByLabel(/^Show sort order/);
await expect(sortOrderVisibleCheckbox).toBeChecked();
await sortOrderVisibleCheckbox.click();
await expect(sortOrderVisibleCheckbox).not.toBeChecked();
// Save settings & close
await settingsScreen.okayButton.click();
await mainScreen.waitFor();
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).not.toBeVisible();
});
});

View File

@@ -0,0 +1,20 @@
import { Page, Locator } from '@playwright/test';
import NoteEditorScreen from './NoteEditorScreen';
export default class MainScreen {
public readonly newNoteButton: Locator;
public readonly noteListContainer: Locator;
public readonly noteEditor: NoteEditorScreen;
public constructor(page: Page) {
this.newNoteButton = page.locator('.new-note-button');
this.noteListContainer = page.locator('.rli-noteList');
this.noteEditor = new NoteEditorScreen(page);
}
public async waitFor() {
await this.newNoteButton.waitFor();
await this.noteEditor.waitFor();
await this.noteListContainer.waitFor();
}
}

View File

@@ -0,0 +1,26 @@
import { Locator, Page } from '@playwright/test';
export default class NoteEditorPage {
public readonly codeMirrorEditor: Locator;
public readonly noteTitleInput: Locator;
private readonly containerLocator: Locator;
public constructor(private readonly page: Page) {
this.containerLocator = page.locator('.rli-editor');
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
this.noteTitleInput = this.containerLocator.locator('.title-input');
}
public getNoteViewerIframe() {
// The note viewer can change content when the note re-renders. As such,
// a new locator needs to be created after re-renders (and this can't be a
// static property).
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
}
public async waitFor() {
await this.codeMirrorEditor.waitFor();
await this.noteTitleInput.waitFor();
}
}

View File

@@ -0,0 +1,17 @@
import { Page, Locator } from '@playwright/test';
export default class SettingsScreen {
public readonly okayButton: Locator;
public readonly appearanceTabButton: Locator;
public constructor(page: Page) {
this.okayButton = page.locator('button', { hasText: 'OK' });
this.appearanceTabButton = page.getByText('Appearance');
}
public async waitFor() {
await this.okayButton.waitFor();
await this.appearanceTabButton.waitFor();
}
}

View File

@@ -0,0 +1,13 @@
#!/bin/sh
echo "Running desktop integration tests..."
export CI=true
if test "$RUNNER_OS" = "Linux" ; then
# The Ubuntu Github CI doesn't have a display server.
# Start a virtual one with xvfb-run.
xvfb-run -- yarn run playwright test
else
yarn run playwright test
fi

View File

@@ -0,0 +1,36 @@
import type { ElectronApplication } from '@playwright/test';
import type { MenuItem } from 'electron';
// Roughly based on
// https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/menu_helpers.ts
// `menuItemPath` should be a list of menu labels (e.g. [["&JoplinMainMenu", "&File"], "Synchronise"]).
const activateMainMenuItem = (electronApp: ElectronApplication, menuItemLabel: string) => {
return electronApp.evaluate(async ({ Menu }, menuItemLabel) => {
const activateItemInSubmenu = (submenu: MenuItem[]) => {
for (const item of submenu) {
if (item.label === menuItemLabel && item.visible) {
// Found!
item.click();
return true;
} else if (item.submenu) {
const foundItem = activateItemInSubmenu(item.submenu.items);
if (foundItem) {
return true;
}
}
}
// No item found
return false;
};
const appMenu = Menu.getApplicationMenu();
return activateItemInSubmenu(appMenu.items);
}, menuItemLabel);
};
export default activateMainMenuItem;

View File

@@ -0,0 +1,45 @@
import { resolve, join, dirname } from 'path';
import { remove, mkdirp } from 'fs-extra';
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
import uuid from '@joplin/lib/uuid';
type JoplinFixtures = {
electronApp: ElectronApplication;
mainWindow: Page;
};
// A custom fixture that loads an electron app. See
// https://playwright.dev/docs/test-fixtures
export const test = base.extend<JoplinFixtures>({
// Playwright fails if we don't use the object destructuring
// pattern in the first argument.
//
// See https://github.com/microsoft/playwright/issues/8798
//
// eslint-disable-next-line no-empty-pattern
electronApp: async ({ }, use) => {
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
const profileSubdir = join(profilePath, uuid.createNano());
await mkdirp(profileSubdir);
const startupArgs = ['main.js', '--env', 'dev', '--profile', profileSubdir];
const electronApp = await electron.launch({ args: startupArgs });
await use(electronApp);
await electronApp.firstWindow();
await electronApp.close();
await remove(profileSubdir);
},
mainWindow: async ({ electronApp }, use) => {
const window = await electronApp.firstWindow();
await use(window);
},
});
export { expect } from '@playwright/test';

View File

@@ -14,7 +14,8 @@
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
"start": "gulp build && electron . --env dev --log-level debug --open-dev-tools",
"test": "jest",
"test-ci": "yarn test",
"test-ui": "playwright test",
"test-ci": "yarn test && sh ./integration-tests/run-ci.sh",
"renameReleaseAssets": "node tools/renameReleaseAssets.js"
},
"repository": {
@@ -116,15 +117,16 @@
"devDependencies": {
"@electron/rebuild": "3.3.0",
"@joplin/tools": "~2.13",
"@playwright/test": "1.38.1",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.4",
"@types/node": "18.17.17",
"@types/react": "18.2.21",
"@types/react-redux": "7.1.26",
"@types/styled-components": "5.1.27",
"@types/node": "18.17.19",
"@types/react": "18.2.23",
"@types/react-redux": "7.1.27",
"@types/styled-components": "5.1.28",
"electron": "25.8.1",
"electron-builder": "24.4.0",
"glob": "10.3.4",
"glob": "10.3.10",
"gulp": "4.0.2",
"jest": "29.6.4",
"jest-environment-jsdom": "29.6.4",
@@ -147,7 +149,7 @@
"@joplin/lib": "~2.13",
"@joplin/renderer": "~2.13",
"@joplin/utils": "~2.13",
"@types/mustache": "4.2.2",
"@types/mustache": "4.2.3",
"async-mutex": "0.4.0",
"codemirror": "5.65.9",
"color": "3.2.1",
@@ -173,7 +175,7 @@
"react-datetime": "3.2.0",
"react-dom": "18.2.0",
"react-redux": "8.1.2",
"react-select": "5.7.4",
"react-select": "5.7.5",
"react-toggle-button": "2.2.0",
"react-tooltip": "4.5.1",
"redux": "4.2.1",

View File

@@ -0,0 +1,34 @@
import { defineConfig } from '@playwright/test';
// See https://playwright.dev/docs/test-configuration.
export default defineConfig({
testDir: './integration-tests',
// Only match .ts files (no compiled .js files)
testMatch: '*.spec.ts',
// Allow running tests in parallel (note: each Joplin instance
// is given its own profile directory).
fullyParallel: true,
// Fail the build on CI if you accidentally left test.only in the source code.
forbidOnly: !!process.env.CI,
// Retry on CI only
retries: process.env.CI ? 2 : 0,
// Opt out of parallel tests on CI
workers: process.env.CI ? 1 : undefined,
// Reporter to use. See https://playwright.dev/docs/test-reporters
reporter: process.env.CI ? 'line' : 'html',
// Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions.
use: {
// Base URL to use in actions like `await page.goto('/')`.
// baseURL: 'http://127.0.0.1:3000',
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
trace: 'on-first-retry',
},
});

View File

@@ -110,8 +110,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097721
versionName "2.13.1"
versionCode 2097722
versionName "2.13.2"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -49,7 +49,7 @@ const ActionButton = (props: ActionButtonProps) => {
};
}), [props.buttons]);
const closedIcon = useIcon(props.mainButton?.icon ?? 'md-add');
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
const openIcon = useIcon('close');
return (

View File

@@ -158,11 +158,11 @@ class CameraView extends Component {
}
public render() {
const photoIcon = this.state.snapping ? 'md-checkmark' : 'md-camera';
const photoIcon = this.state.snapping ? 'checkmark' : 'camera';
const displayRatios = this.supportsRatios() && this.state.ratios.length > 1;
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'md-camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
const ratioButton = !displayRatios ? <View style={{ flex: 1 }}/> : this.renderButton(this.ratio_onPress, <Text style={{ fontWeight: 'bold', fontSize: 20 }}>{Setting.value('camera.ratio')}</Text>, { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', marginRight: 20 });
let cameraRatio = '4:3';
@@ -202,7 +202,7 @@ class CameraView extends Component {
<TouchableOpacity onPress={this.back_onPress}>
<View style={{ marginLeft: 5, marginTop: 5, borderColor: '#00000040', borderWidth: 1, borderStyle: 'solid', borderRadius: 90, width: 50, height: 50, display: 'flex', backgroundColor: '#ffffff77', justifyContent: 'center', alignItems: 'center' }}>
<Icon
name={'md-arrow-back'}
name={'arrow-back'}
style={{
fontSize: 40,
color: 'black',

View File

@@ -308,7 +308,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
accessibilityHint={_('Show/hide the sidebar')}
accessibilityRole="button">
<View style={styles.sideMenuButton}>
<Icon name="md-menu" style={styles.topIcon} />
<Icon name="menu" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
@@ -324,7 +324,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
accessibilityRole="button">
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
<Icon
name="md-arrow-back"
name="arrow-back"
style={styles.topIcon}
/>
</View>
@@ -337,7 +337,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
) {
if (!show) return null;
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
const icon = disabled ? <Icon name="checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
return (
<TouchableOpacity
@@ -407,7 +407,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
description={_('Select all')}
contentStyle={styles.iconButton}
>
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
<Icon name="checkmark-circle-outline" style={styles.topIcon} />
</CustomButton>
);
}
@@ -421,7 +421,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
themeId={themeId}
contentStyle={styles.iconButton}
>
<Icon name="md-search" style={styles.topIcon} />
<Icon name="search" style={styles.topIcon} />
</CustomButton>
);
}
@@ -439,7 +439,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
}
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
>
<Icon name="md-trash" style={styles.topIcon} />
<Icon name="trash" style={styles.topIcon} />
</CustomButton>
);
}
@@ -457,7 +457,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
}
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
>
<Icon name="md-copy" style={styles.topIcon} />
<Icon name="copy" style={styles.topIcon} />
</CustomButton>
);
}
@@ -602,7 +602,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
<Menu onSelect={value => this.menu_select(value)} style={this.styles().contextMenu}>
<MenuTrigger style={contextMenuStyle}>
<View accessibilityLabel={_('Actions')}>
<Icon name="md-ellipsis-vertical" style={this.styles().contextMenuTrigger} />
<Icon name="ellipsis-vertical" style={this.styles().contextMenuTrigger} />
</View>
</MenuTrigger>
<MenuOptions>

View File

@@ -40,7 +40,7 @@ class Checkbox extends Component {
}
render() {
const iconName = this.state.checked ? 'md-checkbox-outline' : 'md-square-outline';
const iconName = this.state.checked ? 'checkbox-outline' : 'square-outline';
const style = this.props.style ? { ...this.props.style } : {};
style.justifyContent = 'center';

View File

@@ -1406,7 +1406,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const editButton = {
label: _('Edit'),
icon: 'md-create',
icon: 'create',
onPress: () => {
this.setState({ mode: 'edit' });

View File

@@ -58,7 +58,7 @@ class NoteTagsDialogComponent extends React.Component {
this.renderTag = data => {
const tag = data.item;
const iconName = noteHasTag(tag.id) ? 'md-checkbox-outline' : 'md-square-outline';
const iconName = noteHasTag(tag.id) ? 'checkbox-outline' : 'square-outline';
return (
<TouchableOpacity key={tag.id} onPress={() => this.tag_press(tag.id)} style={this.styles().tag}>
<View style={this.styles().tagIconText}>

View File

@@ -236,7 +236,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
void this.newNoteNavigate(buttonFolderId, isTodo);
},
color: '#9b59b6',
icon: 'md-checkbox-outline',
icon: 'checkbox-outline',
});
buttons.push({
@@ -246,7 +246,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
void this.newNoteNavigate(buttonFolderId, isTodo);
},
color: '#9b59b6',
icon: 'md-document',
icon: 'document',
});
return <ActionButton buttons={buttons}/>;
}

View File

@@ -190,7 +190,7 @@ class SearchScreenComponent extends BaseScreenComponent {
onPress={() => this.clearButton_press()}
accessibilityLabel={_('Clear')}
>
<Icon name="md-close-circle" style={this.styles().clearIcon} />
<Icon name="close-circle" style={this.styles().clearIcon} />
</TouchableHighlight>
</View>

View File

@@ -101,7 +101,7 @@ const SideMenuContentComponent = (props: Props) => {
styles.sideButtonSelected = { ...styles.sideButton, backgroundColor: theme.selectedColor };
styles.sideButtonText = { ...styles.buttonText };
styles.emptyFolderIcon = { ...styles.sidebarIcon, marginRight: folderIconRightMargin, width: 20 };
styles.emptyFolderIcon = { ...styles.sidebarIcon, marginRight: folderIconRightMargin, width: 21 };
return StyleSheet.create(styles);
}, [props.themeId]);
@@ -421,15 +421,15 @@ const SideMenuContentComponent = (props: Props) => {
items.push(makeDivider('divider_1'));
items.push(renderSidebarButton('newFolder_button', _('New Notebook'), 'md-folder-open', newFolderButton_press));
items.push(renderSidebarButton('newFolder_button', _('New Notebook'), 'folder-open', newFolderButton_press));
items.push(renderSidebarButton('tag_button', _('Tags'), 'md-pricetag', tagButton_press));
items.push(renderSidebarButton('tag_button', _('Tags'), 'pricetag', tagButton_press));
if (props.profileConfig && props.profileConfig.profiles.length > 1) {
items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'md-people-circle-outline', switchProfileButton_press));
items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'people-circle-outline', switchProfileButton_press));
}
items.push(renderSidebarButton('config_button', _('Configuration'), 'md-settings', configButton_press));
items.push(renderSidebarButton('config_button', _('Configuration'), 'settings', configButton_press));
items.push(makeDivider('divider_2'));
@@ -451,7 +451,7 @@ const SideMenuContentComponent = (props: Props) => {
if (resourceFetcherText) fullReport.push(resourceFetcherText);
if (decryptionReportText) fullReport.push(decryptionReportText);
items.push(renderSidebarButton('synchronize_button', !props.syncStarted ? _('Synchronise') : _('Cancel'), 'md-sync', synchronize_press));
items.push(renderSidebarButton('synchronize_button', !props.syncStarted ? _('Synchronise') : _('Cancel'), 'sync', synchronize_press));
if (fullReport.length) {
items.push(
@@ -480,11 +480,11 @@ const SideMenuContentComponent = (props: Props) => {
// using padding. So instead creating blank elements for padding bottom and top.
items.push(<View style={{ height: theme.marginTop }} key="bottom_top_hack" />);
items.push(renderSidebarButton('all_notes', _('All notes'), 'md-document', allNotesButton_press, props.notesParentType === 'SmartFilter'));
items.push(renderSidebarButton('all_notes', _('All notes'), 'document', allNotesButton_press, props.notesParentType === 'SmartFilter'));
items.push(makeDivider('divider_all'));
items.push(renderSidebarButton('folder_header', _('Notebooks'), 'md-folder'));
items.push(renderSidebarButton('folder_header', _('Notebooks'), 'folder'));
if (props.folders.length) {
const result = shared.renderFolders(props, renderFolderItem, false);

View File

@@ -288,7 +288,7 @@ PODS:
- React-Core
- react-native-get-random-values (1.9.0):
- React-Core
- react-native-image-picker (5.6.1):
- react-native-image-picker (5.7.0):
- React-Core
- react-native-image-resizer (3.0.7):
- React-Core
@@ -306,7 +306,7 @@ PODS:
- React-Core
- react-native-version-info (1.1.1):
- React-Core
- react-native-webview (13.4.0):
- react-native-webview (13.5.1):
- React-Core
- React-perflogger (0.71.10)
- React-RCTActionSheet (0.71.10):
@@ -398,9 +398,9 @@ PODS:
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
- RNDateTimePicker (7.4.2):
- RNDateTimePicker (7.5.0):
- React-Core
- RNDeviceInfo (10.8.0):
- RNDeviceInfo (10.9.0):
- React-Core
- RNExitApp (2.0.0):
- React-Core
@@ -414,15 +414,15 @@ PODS:
- React
- RNSecureRandom (1.0.1):
- React
- RNShare (9.2.4):
- RNShare (9.4.1):
- React-Core
- RNVectorIcons (10.0.0):
- React-Core
- RNZipArchive (6.0.9):
- RNZipArchive (6.1.0):
- React-Core
- RNZipArchive/Core (= 6.0.9)
- RNZipArchive/Core (= 6.1.0)
- SSZipArchive (~> 2.2)
- RNZipArchive/Core (6.0.9):
- RNZipArchive/Core (6.1.0):
- React-Core
- SSZipArchive (~> 2.2)
- SSZipArchive (2.4.3)
@@ -669,7 +669,7 @@ SPEC CHECKSUMS:
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-image-picker: 5fcac5a5ffcb3737837f0617d43fd767249290de
react-native-image-picker: 3269f75c251cdcd61ab51b911dd30d6fff8c6169
react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
@@ -678,7 +678,7 @@ SPEC CHECKSUMS:
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
react-native-webview: 64c9bf9646e7377240fb87d70f74556af6433143
react-native-webview: 8baa0f5c6d336d6ba488e942bcadea5bf51f050a
React-perflogger: 217095464d5c4bb70df0742fa86bf2a363693468
React-RCTActionSheet: 8deae9b85a4cbc6a2243618ea62a374880a2c614
React-RCTAnimation: 59c62353a8b59ce206044786c5d30e4754bffa64
@@ -695,17 +695,17 @@ SPEC CHECKSUMS:
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
RNDateTimePicker: a21bfcc694e3190f8ddb5aacb0dd16f97b6be79f
RNDeviceInfo: 5795b418ed3451ebcaf39384e6cf51f60cb931c9
RNDateTimePicker: b722c23030b744763cf51d80fc22f354653f65e0
RNDeviceInfo: 02ea8b23e2280fa18e00a06d7e62804d74028579
RNExitApp: 00036cabe7bacbb413d276d5520bf74ba39afa6a
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNLocalize: dbea38dcb344bf80ff18a1757b1becf11f70cae4
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
RNShare: 255701a138fcdef6c27dc93724548fd122e91f9e
RNShare: 32e97adc8d8c97d4a26bcdd3c45516882184f8b6
RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9
RNZipArchive: 68a0c6db4b1c103f846f1559622050df254a3ade
RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
Yoga: e7ea9e590e27460d28911403b894722354d73479

View File

@@ -26,8 +26,8 @@
"@joplin/renderer": "~2.13",
"@joplin/utils": "~2.13",
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/datetimepicker": "7.4.2",
"@react-native-community/geolocation": "3.0.6",
"@react-native-community/datetimepicker": "7.5.0",
"@react-native-community/geolocation": "3.1.0",
"@react-native-community/netinfo": "9.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-community/slider": "4.4.3",
@@ -46,7 +46,7 @@
"react": "18.2.0",
"react-native": "0.71.10",
"react-native-camera": "4.2.1",
"react-native-device-info": "10.8.0",
"react-native-device-info": "10.9.0",
"react-native-dialogbox": "0.6.10",
"react-native-document-picker": "9.0.1",
"react-native-dropdownalert": "4.5.1",
@@ -55,24 +55,24 @@
"react-native-fingerprint-scanner": "6.0.0",
"react-native-fs": "2.20.0",
"react-native-get-random-values": "1.9.0",
"react-native-image-picker": "5.6.1",
"react-native-image-picker": "5.7.0",
"react-native-localize": "3.0.2",
"react-native-modal-datetime-picker": "17.1.0",
"react-native-paper": "5.10.4",
"react-native-paper": "5.10.6",
"react-native-popup-menu": "0.16.1",
"react-native-quick-actions": "0.3.13",
"react-native-rsa-native": "2.0.5",
"react-native-safe-area-context": "4.7.2",
"react-native-securerandom": "1.0.1",
"react-native-share": "9.2.4",
"react-native-share": "9.4.1",
"react-native-side-menu-updated": "1.3.2",
"react-native-sqlite-storage": "6.0.1",
"react-native-url-polyfill": "2.0.0",
"react-native-vector-icons": "10.0.0",
"react-native-version-info": "1.1.1",
"react-native-vosk": "0.1.12",
"react-native-webview": "13.4.0",
"react-native-zip-archive": "6.0.9",
"react-native-webview": "13.6.0",
"react-native-zip-archive": "6.1.0",
"react-redux": "8.1.2",
"redux": "4.2.1",
"rn-fetch-blob": "0.12.0",
@@ -91,13 +91,13 @@
"@js-draw/material-icons": "1.5.0",
"@lezer/highlight": "1.1.4",
"@testing-library/jest-native": "5.4.3",
"@testing-library/react-native": "12.2.2",
"@testing-library/react-native": "12.3.0",
"@tsconfig/react-native": "2.0.2",
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
"@types/react": "18.2.21",
"@types/react": "18.2.23",
"@types/react-native": "0.70.6",
"@types/react-redux": "7.1.26",
"@types/react-redux": "7.1.27",
"@types/tar-stream": "2.2.3",
"babel-jest": "29.6.4",
"babel-plugin-module-resolver": "4.1.0",

View File

@@ -18,9 +18,9 @@
"@joplin/lib": "~2.13",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.4",
"@types/react": "18.0.24",
"@types/react-redux": "7.1.26",
"@types/styled-components": "5.1.27",
"@types/react": "18.2.23",
"@types/react-redux": "7.1.27",
"@types/styled-components": "5.1.28",
"jest": "29.6.3",
"jest-environment-jsdom": "29.6.3",
"ts-jest": "29.1.1",

View File

@@ -46,7 +46,7 @@
},
"devDependencies": {
"@types/jest": "29.5.4",
"@types/node": "18.17.17",
"@types/node": "18.17.19",
"@typescript-eslint/eslint-plugin": "6.0.0",
"@typescript-eslint/parser": "6.0.0",
"coveralls": "3.1.1",

View File

@@ -16,7 +16,7 @@
],
"devDependencies": {
"standard": "17.1.0",
"tap": "16.3.8"
"tap": "16.3.9"
},
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
}

View File

@@ -0,0 +1,135 @@
function absoluteUrl(url: string) {
if (!url) return url;
const protocol = url.toLowerCase().split(':')[0];
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
if (url.indexOf('//') === 0) {
return location.protocol + url;
} else if (url[0] === '/') {
return `${location.protocol}//${location.host}${url}`;
} else {
return `${baseUrl()}/${url}`;
}
}
function pageLocationOrigin() {
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
// in this case.
if (location.protocol === 'file:') {
return 'file://';
} else {
return location.origin;
}
}
function baseUrl() {
let output = pageLocationOrigin() + location.pathname;
if (output[output.length - 1] !== '/') {
const output2 = output.split('/');
output2.pop();
output = output2.join('/');
}
return output;
}
function getJoplinClipperSvgClassName(svg: SVGSVGElement) {
for (const className of svg.classList) {
if (className.indexOf('joplin-clipper-svg-') === 0) return className;
}
return '';
}
type ImageObject = {
width: number;
height: number;
naturalWidth?: number;
naturalHeight?: number;
};
export function getImageSizes(element: Document, forceAbsoluteUrls = false) {
const output: Record<string, ImageObject[]> = {};
const images = element.getElementsByTagName('img');
for (let i = 0; i < images.length; i++) {
const img = images[i];
if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
let src = imageSrc(img);
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
if (!output[src]) output[src] = [];
output[src].push({
width: img.width,
height: img.height,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
});
}
const svgs = element.getElementsByTagName('svg');
for (let i = 0; i < svgs.length; i++) {
const svg = svgs[i];
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) continue;
const className = getJoplinClipperSvgClassName(svg);// 'joplin-clipper-svg-' + i;
if (!className) {
console.warn('SVG without a Joplin class:', svg);
continue;
}
if (!svg.classList.contains(className)) {
svg.classList.add(className);
}
const rect = svg.getBoundingClientRect();
if (!output[className]) output[className] = [];
output[className].push({
width: rect.width,
height: rect.height,
});
}
return output;
}
// In general we should use currentSrc because that's the image that's currently displayed,
// especially within <picture> tags or with srcset. In these cases there can be multiple
// sources and the best one is probably the one being displayed, thus currentSrc.
function imageSrc(image: HTMLImageElement) {
if (image.currentSrc) return image.currentSrc;
return image.src;
}
// Given a document, return a <style> tag that contains all the styles
// required to render the page. Not currently used but could be as an
// option to clip pages as HTML.
// eslint-disable-next-line
export function getStyleSheets(doc: Document) {
const output = [];
for (let i = 0; i < doc.styleSheets.length; i++) {
const sheet = doc.styleSheets[i];
try {
for (const cssRule of sheet.cssRules) {
output.push({ type: 'text', value: cssRule.cssText });
}
} catch (error) {
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
// by the desktop application, since it doesn't have CORS restrictions.
// eslint-disable-next-line
console.info('Could not retrieve stylesheet now:', sheet.href);
// eslint-disable-next-line
console.info('It will downloaded by the main application.');
// eslint-disable-next-line
console.info(error);
output.push({ type: 'url', value: sheet.href });
}
}
return output;
}

View File

@@ -1,4 +1,4 @@
import { closestSupportedLocale } from './locale';
import { closestSupportedLocale, parsePluralForm, setLocale, _n } from './locale';
describe('locale', () => {
@@ -15,4 +15,80 @@ describe('locale', () => {
}
});
it('should translate plurals - en_GB', () => {
setLocale('en_GB');
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Copy Shareable Link');
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Copy Shareable Links');
});
it('should translate plurals - fr_FR', () => {
setLocale('fr_FR');
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Copier lien partageable');
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Copier liens partageables');
});
it('should translate plurals - pl_PL', () => {
setLocale('pl_PL');
// Not the best test since 5 is the same as 2, but it's all I could find
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Kopiuj udostępnialny link');
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Kopiuj udostępnialne linki');
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 5)).toBe('Kopiuj udostępnialne linki');
});
it('should parse the plural form', async () => {
const pluralForms = [
'nplurals=1; plural=0;',
'nplurals=2; plural=(n != 0);',
'nplurals=2; plural=(n != 1);',
'nplurals=2; plural=(n > 1);',
'nplurals=2; plural=(n%10!=1 || n%100==11);',
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);',
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);',
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
'nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);',
'nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2);',
'nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;',
'nplurals=3; plural=(n==1) ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;',
'nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);',
'nplurals=4; plural=(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3);',
'nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;',
'nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;',
'nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3;',
'nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>6 && n<11) ? 3 : 4;',
'nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);',
];
const pluralValues = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1],
[2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1],
[2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1],
[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2],
[0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2],
[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2],
[0, 1, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3],
[3, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
[2, 0, 1, 2, 2, 2, 2, 2, 3, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[3, 0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
[4, 0, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
[0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
];
for (let index = 0; index < pluralForms.length; index++) {
const form = pluralForms[index];
const pluralFn = parsePluralForm(form);
for (let i = 0; i < 128; ++i) {
expect(pluralValues[index][i]).toBe(pluralFn(i));
}
}
});
});

View File

@@ -7,6 +7,7 @@ interface StringToStringMap {
interface CodeToCountryMap {
[key: string]: string[];
}
type ParsePluralFormFunction = (n: number)=> number;
const codeToLanguageE_: StringToStringMap = {};
codeToLanguageE_['aa'] = 'Afar';
@@ -436,12 +437,51 @@ const codeToCountry_: CodeToCountryMap = {
let supportedLocales_: any = null;
let localeStats_: any = null;
const loadedLocales_: any = {};
const loadedLocales_: Record<string, Record<string, string[]>> = {};
const pluralFunctions_: Record<string, ParsePluralFormFunction> = {};
const defaultLocale_ = 'en_GB';
let currentLocale_ = defaultLocale_;
// Copied from https://github.com/eugeny-dementev/parse-gettext-plural-form
// along with the tests
export const parsePluralForm = (form: string): ParsePluralFormFunction => {
const pluralFormRegex = /^(\s*nplurals\s*=\s*[0-9]+\s*;\s*plural\s*=\s*(?:\s|[-?|&=!<>+*/%:;a-zA-Z0-9_()])+)$/m;
if (!pluralFormRegex.test(form)) throw new Error(`Plural-Forms is invalid: ${form}`);
if (!/;\s*$/.test(form)) {
form += ';';
}
const code = [
'var plural;',
'var nplurals;',
form,
'return (plural === true ? 1 : plural ? plural : 0);',
].join('\n');
// eslint-disable-next-line no-new-func -- There's a regex to check the form but it's still slighlty unsafe, eventually we should automatically generate all the functions in advance in build-translations.ts
return (new Function('n', code)) as ParsePluralFormFunction;
};
const getPluralFunction = (lang: string) => {
if (!(lang in pluralFunctions_)) {
const locale = closestSupportedLocale(lang);
const stats = localeStats()[locale];
if (!stats.pluralForms) {
pluralFunctions_[lang] = null;
} else {
pluralFunctions_[lang] = parsePluralForm(stats.pluralForms);
}
}
return pluralFunctions_[lang];
};
function defaultLocale() {
return defaultLocale_;
}
@@ -589,18 +629,45 @@ function _(s: string, ...args: any[]): string {
}
function _n(singular: string, plural: string, n: number, ...args: any[]) {
if (n > 1) return _(plural, ...args);
return _(singular, ...args);
if (['en_GB', 'en_US'].includes(currentLocale_)) {
if (n > 1) return _(plural, ...args);
return _(singular, ...args);
} else {
const pluralFn = getPluralFunction(currentLocale_);
const stringIndex = pluralFn ? pluralFn(n) : 0;
const strings = localeStrings(currentLocale_);
const result = strings[singular];
let translatedString = '';
if (result === undefined || !result.join('')) {
translatedString = singular;
} else {
translatedString = stringIndex < result.length ? result[stringIndex] : result[0];
}
try {
return sprintf(translatedString, ...args);
} catch (error) {
return `${translatedString} ${args.join(', ')} (Translation error: ${error.message})`;
}
}
}
const stringByLocale = (locale: string, s: string, ...args: any[]): string => {
const strings = localeStrings(locale);
let result = strings[s];
if (result === '' || result === undefined) result = s;
const result = strings[s];
let translatedString = '';
if (result === undefined || !result.join('')) {
translatedString = s;
} else {
translatedString = result[0];
}
try {
return sprintf(result, ...args);
return sprintf(translatedString, ...args);
} catch (error) {
return `${result} ${args.join(', ')} (Translation error: ${error.message})`;
return `${translatedString} ${args.join(', ')} (Translation error: ${error.message})`;
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -41,45 +41,45 @@ locales['uk_UA'] = require('./uk_UA.json');
locales['vi'] = require('./vi.json');
locales['zh_CN'] = require('./zh_CN.json');
locales['zh_TW'] = require('./zh_TW.json');
stats['ar'] = {"percentDone":77};
stats['eu'] = {"percentDone":22};
stats['bs_BA'] = {"percentDone":56};
stats['bg_BG'] = {"percentDone":44};
stats['ca'] = {"percentDone":86};
stats['hr_HR'] = {"percentDone":98};
stats['cs_CZ'] = {"percentDone":96};
stats['da_DK'] = {"percentDone":96};
stats['de_DE'] = {"percentDone":98};
stats['et_EE'] = {"percentDone":43};
stats['ar'] = {"percentDone":84,"pluralForms":"nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);"};
stats['eu'] = {"percentDone":21,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['bs_BA'] = {"percentDone":54,"pluralForms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"};
stats['bg_BG'] = {"percentDone":42,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['ca'] = {"percentDone":92,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['hr_HR'] = {"percentDone":97,"pluralForms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"};
stats['cs_CZ'] = {"percentDone":94,"pluralForms":"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"};
stats['da_DK'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['de_DE'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['et_EE'] = {"percentDone":42,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['en_GB'] = {"percentDone":100};
stats['en_US'] = {"percentDone":100};
stats['es_ES'] = {"percentDone":95};
stats['eo'] = {"percentDone":25};
stats['fi_FI'] = {"percentDone":96};
stats['fr_FR'] = {"percentDone":100};
stats['gl_ES'] = {"percentDone":28};
stats['id_ID'] = {"percentDone":86};
stats['it_IT'] = {"percentDone":78};
stats['hu_HU'] = {"percentDone":75};
stats['nl_BE'] = {"percentDone":76};
stats['nl_NL'] = {"percentDone":86};
stats['nb_NO'] = {"percentDone":85};
stats['fa'] = {"percentDone":53};
stats['pl_PL'] = {"percentDone":88};
stats['pt_BR'] = {"percentDone":85};
stats['pt_PT'] = {"percentDone":70};
stats['ro'] = {"percentDone":49};
stats['sl_SI'] = {"percentDone":78};
stats['sv'] = {"percentDone":97};
stats['th_TH'] = {"percentDone":35};
stats['vi'] = {"percentDone":75};
stats['tr_TR'] = {"percentDone":97};
stats['uk_UA'] = {"percentDone":98};
stats['el_GR'] = {"percentDone":97};
stats['ru_RU'] = {"percentDone":97};
stats['sr_RS'] = {"percentDone":63};
stats['zh_CN'] = {"percentDone":98};
stats['zh_TW'] = {"percentDone":86};
stats['ja_JP'] = {"percentDone":88};
stats['ko'] = {"percentDone":86};
stats['en_US'] = {"percentDone":100,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['es_ES'] = {"percentDone":93,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['eo'] = {"percentDone":24,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['fi_FI'] = {"percentDone":94,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['fr_FR'] = {"percentDone":100,"pluralForms":"nplurals=2; plural=(n > 1);"};
stats['gl_ES'] = {"percentDone":27,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['id_ID'] = {"percentDone":84,"pluralForms":"nplurals=1; plural=0;"};
stats['it_IT'] = {"percentDone":76,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['hu_HU'] = {"percentDone":73,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['nl_BE'] = {"percentDone":74,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['nl_NL'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['nb_NO'] = {"percentDone":83,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['fa'] = {"percentDone":52,"pluralForms":"nplurals=2; plural=(n==0 || n==1);"};
stats['pl_PL'] = {"percentDone":85,"pluralForms":"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);"};
stats['pt_BR'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n > 1);"};
stats['pt_PT'] = {"percentDone":68,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['ro'] = {"percentDone":48,"pluralForms":"nplurals=3; plural=(n==1 ? 0 : n==0 || (n!=1 && n%100>=1 && n%100<=19) ? 1 : 2);"};
stats['sl_SI'] = {"percentDone":76,"pluralForms":"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"};
stats['sv'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['th_TH'] = {"percentDone":34,"pluralForms":"nplurals=1; plural=0;"};
stats['vi'] = {"percentDone":73,"pluralForms":"nplurals=1; plural=0;"};
stats['tr_TR'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['uk_UA'] = {"percentDone":95,"pluralForms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);"};
stats['el_GR'] = {"percentDone":94,"pluralForms":"nplurals=2; plural=(n != 1);"};
stats['ru_RU'] = {"percentDone":96,"pluralForms":"nplurals=2; plural=(n > 1);"};
stats['sr_RS'] = {"percentDone":61,"pluralForms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);"};
stats['zh_CN'] = {"percentDone":97,"pluralForms":"nplurals=1; plural=0;"};
stats['zh_TW'] = {"percentDone":84,"pluralForms":"nplurals=1; plural=0;"};
stats['ja_JP'] = {"percentDone":97,"pluralForms":"nplurals=1; plural=0;"};
stats['ko'] = {"percentDone":84,"pluralForms":"nplurals=1; plural=0;"};
module.exports = { locales: locales, stats: stats };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1115,7 +1115,7 @@ class Setting extends BaseModel {
public: true,
appTypes: [AppType.Mobile, AppType.Desktop],
label: () => _('Resize large images:'),
description: () => _('Shrink large images before adding them to notes to save storage space.'),
description: () => _('Shrink large images before adding them to notes.'),
options: () => {
return {
alwaysAsk: _('Always ask'),

View File

@@ -19,13 +19,13 @@
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
"@types/js-yaml": "4.0.6",
"@types/node": "18.17.17",
"@types/node-rsa": "1.1.1",
"@types/react": "18.2.21",
"@types/node": "18.17.19",
"@types/node-rsa": "1.1.2",
"@types/react": "18.2.23",
"@types/uuid": "9.0.4",
"clean-html": "1.5.0",
"jest": "29.6.4",
"sharp": "0.32.5",
"sharp": "0.32.6",
"typescript": "5.1.6"
},
"dependencies": {
@@ -52,7 +52,7 @@
"es6-promise-pool": "2.5.0",
"fast-deep-equal": "3.1.3",
"fast-xml-parser": "3.21.1",
"follow-redirects": "1.15.2",
"follow-redirects": "1.15.3",
"form-data": "4.0.0",
"fs-extra": "11.1.1",
"hpagent": "1.2.0",
@@ -63,7 +63,7 @@
"immer": "7.0.15",
"js-yaml": "4.1.0",
"levenshtein": "1.0.5",
"markdown-it": "13.0.1",
"markdown-it": "13.0.2",
"md5": "2.3.0",
"md5-file": "5.0.0",
"moment": "2.29.4",
@@ -86,7 +86,7 @@
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
"string-to-stream": "3.0.1",
"tar": "6.1.15",
"tar": "6.2.0",
"tcp-port-used": "1.0.2",
"uglifycss": "0.0.29",
"url-parse": "1.5.10",

View File

@@ -27,6 +27,7 @@ const { fileExtension, safeFileExtension, safeFilename, filename } = require('..
const { MarkupToHtml } = require('@joplin/renderer');
const { ErrorNotFound } = require('../utils/errors');
import { fileUriToPath } from '@joplin/utils/url';
import { NoteEntity } from '../../database/types';
const logger = Logger.create('routes/notes');
@@ -38,7 +39,31 @@ function htmlToMdParser() {
return htmlToMdParser_;
}
async function requestNoteToNote(requestNote: any) {
type RequestNote = {
id?: any;
parent_id?: string;
title: string;
body?: string;
latitude?: number;
longitude?: number;
altitude?: number;
author?: string;
source_url?: string;
is_todo?: number;
todo_due?: number;
todo_completed?: number;
user_updated_time?: number;
user_created_time?: number;
markup_language?: number;
body_html: string;
base_url?: string;
convert_to: string;
anchor_names?: any[];
image_sizes?: object;
stylesheets: any;
};
async function requestNoteToNote(requestNote: RequestNote): Promise<NoteEntity> {
const output: any = {
title: requestNote.title ? requestNote.title : '',
body: requestNote.body ? requestNote.body : '',
@@ -337,6 +362,34 @@ async function attachImageFromDataUrl(note: any, imageDataUrl: string, cropRect:
return await shim.attachFileToNote(note, tempFilePath);
}
export const extractNoteFromHTML = async (requestNote: RequestNote, requestId: number, imageSizes: any) => {
const note = await requestNoteToNote(requestNote);
const mediaUrls = extractMediaUrls(note.markup_language, note.body);
logger.info(`Request (${requestId}): Downloading media files: ${mediaUrls.length}`);
const mediaFiles = await downloadMediaFiles(mediaUrls); // , allowFileProtocolImages);
logger.info(`Request (${requestId}): Creating resources from paths: ${Object.getOwnPropertyNames(mediaFiles).length}`);
const resources = await createResourcesFromPaths(mediaFiles);
await removeTempFiles(resources);
note.body = replaceUrlsByResources(note.markup_language, note.body, resources, imageSizes);
logger.info(`Request (${requestId}): Saving note...`);
const saveOptions = defaultSaveOptions('POST', note.id);
saveOptions.autoTimestamp = false; // No auto-timestamp because user may have provided them
const timestamp = Date.now();
note.updated_time = timestamp;
note.created_time = timestamp;
if (!('user_updated_time' in note)) note.user_updated_time = timestamp;
if (!('user_created_time' in note)) note.user_created_time = timestamp;
return { note, saveOptions, resources };
};
export default async function(request: Request, id: string = null, link: string = null) {
if (request.method === 'GET') {
if (link && link === 'tags') {
@@ -368,31 +421,9 @@ export default async function(request: Request, id: string = null, link: string
logger.info('Images:', imageSizes);
let note: any = await requestNoteToNote(requestNote);
const extracted = await extractNoteFromHTML(requestNote, requestId, imageSizes);
const mediaUrls = extractMediaUrls(note.markup_language, note.body);
logger.info(`Request (${requestId}): Downloading media files: ${mediaUrls.length}`);
let result = await downloadMediaFiles(mediaUrls); // , allowFileProtocolImages);
logger.info(`Request (${requestId}): Creating resources from paths: ${Object.getOwnPropertyNames(result).length}`);
result = await createResourcesFromPaths(result);
await removeTempFiles(result);
note.body = replaceUrlsByResources(note.markup_language, note.body, result, imageSizes);
logger.info(`Request (${requestId}): Saving note...`);
const saveOptions = defaultSaveOptions('POST', note.id);
saveOptions.autoTimestamp = false; // No auto-timestamp because user may have provided them
const timestamp = Date.now();
note.updated_time = timestamp;
note.created_time = timestamp;
if (!('user_updated_time' in note)) note.user_updated_time = timestamp;
if (!('user_created_time' in note)) note.user_created_time = timestamp;
note = await Note.save(note, saveOptions);
let note = await Note.save(extracted.note, extracted.saveOptions);
if (requestNote.tags) {
const tagTitles = requestNote.tags.split(',');

View File

@@ -42,7 +42,7 @@ const shim = {
isNode: () => {
if (typeof process === 'undefined') return false;
if (shim.isElectron()) return true;
return process.title === 'node' || (process.title && process.title.indexOf('gulp') === 0);
return !shim.mobilePlatform();
},
isReactNative: () => {

View File

@@ -21,9 +21,9 @@
"devDependencies": {
"@types/jest": "29.5.4",
"@types/pdfjs-dist": "2.10.378",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"@types/styled-components": "5.1.27",
"@types/react": "18.2.23",
"@types/react-dom": "18.2.8",
"@types/styled-components": "5.1.28",
"babel-jest": "29.6.4",
"css-loader": "6.8.1",
"jest": "29.6.4",

View File

@@ -30,7 +30,7 @@
"devDependencies": {
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
"@types/node": "18.17.17",
"@types/node": "18.17.19",
"jest": "29.6.4",
"source-map-loader": "4.0.1",
"typescript": "5.1.6",

View File

@@ -20,7 +20,7 @@
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@types/jest": "29.5.4",
"@types/node": "18.17.17",
"@types/node": "18.17.19",
"jest": "29.6.4",
"jest-environment-jsdom": "29.6.4",
"ts-jest": "29.1.1",
@@ -36,7 +36,7 @@
"html-entities": "1.4.0",
"json-stringify-safe": "5.0.1",
"katex": "0.16.8",
"markdown-it": "13.0.1",
"markdown-it": "13.0.2",
"markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "5.3.0",
"markdown-it-deflist": "2.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.13.1",
"version": "2.13.2",
"private": true,
"scripts": {
"start-dev": "yarn run build && JOPLIN_IS_TESTING=1 nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
@@ -17,6 +17,7 @@
"test-ci": "yarn test",
"test-debug": "node --inspect node_modules/.bin/jest -- --verbose=false",
"clean": "gulp clean",
"populateDatabase": "JOPLIN_TESTS_SERVER_DB=pg node dist/utils/testing/populateDatabase",
"stripeListen": "stripe listen --forward-to http://joplincloud.local:22300/stripe/webhook",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json"
},
@@ -32,14 +33,14 @@
"bulma": "0.9.4",
"bulma-prefers-dark": "0.1.0-beta.1",
"compare-versions": "6.1.0",
"dayjs": "1.11.9",
"dayjs": "1.11.10",
"formidable": "3.5.1",
"fs-extra": "11.1.1",
"html-entities": "1.4.0",
"jquery": "3.7.1",
"knex": "2.5.1",
"koa": "2.14.2",
"markdown-it": "13.0.1",
"markdown-it": "13.0.2",
"mustache": "4.2.0",
"nanoid": "2.1.11",
"node-cron": "3.0.2",
@@ -60,6 +61,7 @@
"devDependencies": {
"@joplin/tools": "~2.13",
"@rmp135/sql-ts": "1.18.0",
"@types/bcryptjs": "2.4.5",
"@types/formidable": "3.4.3",
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
@@ -67,9 +69,9 @@
"@types/jsdom": "21.1.3",
"@types/koa": "2.13.9",
"@types/markdown-it": "12.2.3",
"@types/mustache": "4.2.2",
"@types/nodemailer": "6.4.10",
"@types/yargs": "17.0.24",
"@types/mustache": "4.2.3",
"@types/nodemailer": "6.4.11",
"@types/yargs": "17.0.26",
"@types/zxcvbn": "4.4.2",
"gulp": "4.0.2",
"jest": "29.6.4",

View File

@@ -98,7 +98,7 @@ export const up = async (db: DbConnection) => {
await db('users').insert({
id: adminId,
email: defaultAdminEmail,
password: hashPassword(defaultAdminPassword),
password: await hashPassword(defaultAdminPassword),
full_name: 'Admin',
is_admin: 1,
updated_time: now,

View File

@@ -197,7 +197,7 @@ export default abstract class BaseModel<T> {
// The `name` argument is only for debugging, so that any stuck transaction
// can be more easily identified.
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
protected async withTransaction<T>(fn: Function, name: string): Promise<T> {
protected async withTransaction<T>(fn: Function, name = ''): Promise<T> {
const debugSteps = false;
const debugTimeout = true;
const timeoutMs = 10000;

View File

@@ -6,7 +6,7 @@ import { md5 } from '../utils/crypto';
import { ErrorResyncRequired } from '../utils/errors';
import { Day, formatDateTime } from '../utils/time';
import BaseModel, { SaveOptions } from './BaseModel';
import { PaginatedResults, Pagination, PaginationOrderDir } from './utils/pagination';
import { PaginatedResults } from './utils/pagination';
const logger = Logger.create('ChangeModel');
@@ -88,7 +88,44 @@ export default class ChangeModel extends BaseModel<Change> {
};
}
private changesForUserQuery(userId: Uuid, count: boolean): Knex.QueryBuilder {
// private changesForUserQuery(userId: Uuid, count: boolean): Knex.QueryBuilder {
// // When need to get:
// //
// // - All the CREATE and DELETE changes associated with the user
// // - All the UPDATE changes that applies to items associated with the
// // user.
// //
// // UPDATE changes do not have the user_id set because they are specific
// // to the item, not to a particular user.
// const query = this
// .db('changes')
// .where(function() {
// void this.whereRaw('((type = ? OR type = ?) AND user_id = ?)', [ChangeType.Create, ChangeType.Delete, userId])
// // Need to use a RAW query here because Knex has a "not a
// // bug" bug that makes it go into infinite loop in some
// // contexts, possibly only when running inside Jest (didn't
// // test outside).
// // https://github.com/knex/knex/issues/1851
// .orWhereRaw('type = ? AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)', [ChangeType.Update, userId]);
// });
// if (count) {
// void query.countDistinct('id', { as: 'total' });
// } else {
// void query.select([
// 'id',
// 'item_id',
// 'item_name',
// 'type',
// 'updated_time',
// ]);
// }
// return query;
// }
public async changesForUserQuery(userId: Uuid, fromCounter: number, limit: number, doCountQuery: boolean): Promise<Change[]> {
// When need to get:
//
// - All the CREATE and DELETE changes associated with the user
@@ -98,61 +135,125 @@ export default class ChangeModel extends BaseModel<Change> {
// UPDATE changes do not have the user_id set because they are specific
// to the item, not to a particular user.
const query = this
.db('changes')
.where(function() {
void this.whereRaw('((type = ? OR type = ?) AND user_id = ?)', [ChangeType.Create, ChangeType.Delete, userId])
// Need to use a RAW query here because Knex has a "not a
// bug" bug that makes it go into infinite loop in some
// contexts, possibly only when running inside Jest (didn't
// test outside).
// https://github.com/knex/knex/issues/1851
.orWhereRaw('type = ? AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)', [ChangeType.Update, userId]);
});
// This used to be just one query but it kept getting slower and slower
// as the `changes` table grew. So it is now split into two queries
// merged by a UNION ALL.
if (count) {
void query.countDistinct('id', { as: 'total' });
const fields = [
'id',
'item_id',
'item_name',
'type',
'updated_time',
'counter',
];
const fieldsSql = `"${fields.join('", "')}"`;
const subQuery1 = `
SELECT ${fieldsSql}
FROM "changes"
WHERE counter > ?
AND (type = ? OR type = ?)
AND user_id = ?
ORDER BY "counter" ASC
${doCountQuery ? '' : 'LIMIT ?'}
`;
const subParams1 = [
fromCounter,
ChangeType.Create,
ChangeType.Delete,
userId,
];
if (!doCountQuery) subParams1.push(limit);
const subQuery2 = `
SELECT ${fieldsSql}
FROM "changes"
WHERE counter > ?
AND type = ?
AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)
ORDER BY "counter" ASC
${doCountQuery ? '' : 'LIMIT ?'}
`;
const subParams2 = [
fromCounter,
ChangeType.Update,
userId,
];
if (!doCountQuery) subParams2.push(limit);
let query: Knex.Raw<any> = null;
const finalParams = subParams1.concat(subParams2);
if (!doCountQuery) {
finalParams.push(limit);
query = this.db.raw(`
SELECT ${fieldsSql} FROM (${subQuery1}) as sub1
UNION ALL
SELECT ${fieldsSql} FROM (${subQuery2}) as sub2
ORDER BY counter ASC
LIMIT ?
`, finalParams);
} else {
void query.select([
'id',
'item_id',
'item_name',
'type',
'updated_time',
]);
query = this.db.raw(`
SELECT count(*) as total
FROM (
(${subQuery1})
UNION ALL
(${subQuery2})
) AS merged
`, finalParams);
}
return query;
const results = await query;
// Because it's a raw query, we need to handle the results manually:
// Postgres returns an object with a "rows" property, while SQLite
// returns the rows directly;
const output: Change[] = results.rows ? results.rows : results;
// This property is present only for the purpose of ordering the results
// and can be removed afterwards.
for (const change of output) delete change.counter;
return output;
}
public async allByUser(userId: Uuid, pagination: Pagination = null): Promise<PaginatedDeltaChanges> {
pagination = {
page: 1,
limit: 100,
order: [{ by: 'counter', dir: PaginationOrderDir.ASC }],
...pagination,
};
// public async allByUser(userId: Uuid, pagination: Pagination = null): Promise<PaginatedDeltaChanges> {
// pagination = {
// page: 1,
// limit: 100,
// order: [{ by: 'counter', dir: PaginationOrderDir.ASC }],
// ...pagination,
// };
const query = this.changesForUserQuery(userId, false);
const countQuery = this.changesForUserQuery(userId, true);
const itemCount = (await countQuery.first()).total;
// const query = this.changesForUserQuery(userId, false);
// const countQuery = this.changesForUserQuery(userId, true);
// const itemCount = (await countQuery.first()).total;
void query
.orderBy(pagination.order[0].by, pagination.order[0].dir)
.offset((pagination.page - 1) * pagination.limit)
.limit(pagination.limit) as any[];
// void query
// .orderBy(pagination.order[0].by, pagination.order[0].dir)
// .offset((pagination.page - 1) * pagination.limit)
// .limit(pagination.limit) as any[];
const changes = await query;
// const changes = await query;
return {
items: changes,
// If we have changes, we return the ID of the latest changes from which delta sync can resume.
// If there's no change, we return the previous cursor.
cursor: changes.length ? changes[changes.length - 1].id : pagination.cursor,
has_more: changes.length >= pagination.limit,
page_count: itemCount !== null ? Math.ceil(itemCount / pagination.limit) : undefined,
};
}
// return {
// items: changes,
// // If we have changes, we return the ID of the latest changes from which delta sync can resume.
// // If there's no change, we return the previous cursor.
// cursor: changes.length ? changes[changes.length - 1].id : pagination.cursor,
// has_more: changes.length >= pagination.limit,
// page_count: itemCount !== null ? Math.ceil(itemCount / pagination.limit) : undefined,
// };
// }
public async delta(userId: Uuid, pagination: ChangePagination = null): Promise<PaginatedDeltaChanges> {
pagination = {
@@ -167,18 +268,12 @@ export default class ChangeModel extends BaseModel<Change> {
if (!changeAtCursor) throw new ErrorResyncRequired();
}
const query = this.changesForUserQuery(userId, false);
// If a cursor was provided, apply it to the query.
if (changeAtCursor) {
void query.where('counter', '>', changeAtCursor.counter);
}
void query
.orderBy('counter', 'asc')
.limit(pagination.limit) as any[];
const changes: Change[] = await query;
const changes = await this.changesForUserQuery(
userId,
changeAtCursor ? changeAtCursor.counter : -1,
pagination.limit,
false,
);
const items: Item[] = await this.db('items').select('id', 'jop_updated_time').whereIn('items.id', changes.map(c => c.item_id));

View File

@@ -21,13 +21,15 @@ export default class TaskStateModel extends BaseModel<TaskState> {
}
public async init(taskId: TaskId) {
const taskState: TaskState = await this.loadByTaskId(taskId);
if (taskState) return taskState;
return this.withTransaction(async () => {
const taskState: TaskState = await this.loadByTaskId(taskId);
if (taskState) return taskState;
return this.save({
task_id: taskId,
enabled: 1,
running: 0,
return this.save({
task_id: taskId,
enabled: 1,
running: 0,
});
});
}

View File

@@ -186,6 +186,9 @@ export default class UserItemModel extends BaseModel<UserItem> {
for (const userItem of userItems) {
const item = items.find(i => i.id === userItem.item_id);
// The item may have been deleted between the async calls above
if (!item) continue;
if (options.recordChanges && this.models().item().shouldRecordChange(item.name)) {
await this.models().change().save({
item_type: ItemType.UserItem,

View File

@@ -428,9 +428,14 @@ describe('UserModel', () => {
test('should throw an error if the password being saved seems to be hashed', async () => {
const passwordSimilarToHash = '$2a$10';
const error = await checkThrowAsync(async () => await models().user().save({ password: passwordSimilarToHash }));
const user = await models().user().save({
email: 'test@example.com',
password: '111111',
});
expect(error.message).toBe('Unable to save user because password already seems to be hashed. User id: undefined');
const error = await checkThrowAsync(async () => await models().user().save({ id: user.id, password: passwordSimilarToHash }));
expect(error.message).toBe(`Unable to save user because password already seems to be hashed. User id: ${user.id}`);
expect(error instanceof ErrorBadRequest).toBe(true);
});

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