1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-14 23:26:58 +02:00

Compare commits

...

61 Commits

Author SHA1 Message Date
Laurent Cozic
620afdaab1 Android 3.5.3 2025-12-14 13:48:19 +00:00
Laurent Cozic
3f8928000e Chore: Clean up Android release script 2025-12-14 13:37:22 +00:00
Laurent Cozic
5caec161f2 Mobile: Add a link to the list of open-source licenses 2025-12-13 00:03:26 +00:00
Laurent Cozic
daab2223e7 Chore: Fix CI 2025-12-12 16:46:53 +00:00
Laurent Cozic
f96071870c All: Fixes #12172: Markdown import incorrectly parses a link as a file path 2025-12-12 14:53:35 +00:00
Laurent Cozic
5e08abb7a9 Desktop: Fixes #12367: When using RTE, switching to a note from go to anything search results with keyboard immediately updates note last modified date 2025-12-12 14:31:50 +00:00
Laurent Cozic
2c71557d88 All: Fixes #12770: Import Error: Note date incorrect when import notes with import MD - Markdown + Front Matter 2025-12-12 12:35:38 +00:00
Laurent Cozic
d551963669 All: Fixes #13008: Importing MD + frontmatter fails on empty variable 2025-12-12 11:53:03 +00:00
Laurent Cozic
7dae90c9f3 Linux: Fixes #13038: Do not suggest downgrading the app when a version has been unpublished 2025-12-12 11:29:40 +00:00
Laurent Cozic
46820fb21b Server: Fixes #13059: Confusing error message if a published note has not been synced to the server 2025-12-12 10:56:22 +00:00
Laurent Cozic
a18e49ab54 Chore: Fixes #13358: Fix randomly failing test on app/command-rmnote.test 2025-12-12 10:27:25 +00:00
Laurent Cozic
2c6eaca442 Desktop: Fixes #13814: Remove architecture warning on Windows ARM with Apple silicon 2025-12-12 10:05:37 +00:00
Laurent Cozic
44de1246d9 Desktop: Fixes #13880: Warning logged on startup when ABC Sheet Music plugin is not installed 2025-12-12 09:49:57 +00:00
renovate[bot]
ab3a0ab69f Update dependency pg-boss to v10.3.3 (#13894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 04:13:14 +00:00
Laurent Cozic
896f0e0bc5 Doc: Clarify difference between JS and JSB 2025-12-11 18:29:56 +00:00
Henry Heino
e2c933db82 Chore: Resolves #13866: Create a tool that lists dependencies of a package and its licenses (#13874) 2025-12-11 15:16:41 +00:00
Henry Heino
30c5031611 Mobile: Rich Text Editor: Fix table delete row/delete column buttons can't remove the last row/column from a table (#13877) 2025-12-11 15:16:13 +00:00
Henry Heino
e7f14a0995 Desktop: Fixes #13872: Fix importing HTML links with multi-line titles as Markdown (#13876) 2025-12-11 15:15:28 +00:00
Henry Heino
319bf79bc1 Mobile: Upgrade js-draw to v1.32.0 (#13875) 2025-12-11 08:17:37 +00:00
renovate[bot]
02f94adb96 Update dependency nodejs to v24.5.0 (#13890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 16:25:17 +00:00
Laurent Cozic
2370c12129 Revert "Update dependency @react-native/babel-preset to v0.81.0" (#13888) 2025-12-10 12:56:15 +00:00
Laurent Cozic
8d074a563b Update renovate.json5 2025-12-10 12:55:48 +00:00
renovate[bot]
1014edfdeb Update dependency @react-native/babel-preset to v0.81.0 (#13886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 12:54:43 +00:00
Laurent Cozic
364bdd9bb0 Chore: Allow specifying a custom API key at build time 2025-12-09 15:33:07 +00:00
Laurent Cozic
8d6b219191 lock file 2025-12-09 15:07:45 +00:00
Laurent Cozic
2455245f86 Chore: Allow creating a custom build of the Android app 2025-12-09 15:07:28 +00:00
Laurent Cozic
c669a3986e Tools: Allow generating Android images using generate-imgae script 2025-12-09 10:53:57 +00:00
Laurent Cozic
5f1a1e50d9 Doc: Add forum background images 2025-12-09 00:06:48 +00:00
Self Not Found
819a591cc0 Desktop: Add CJK characters counter in statistics panel (#13840) 2025-12-08 18:52:09 +00:00
mrjo118
421b82c86d Mobile: Add the ability to rename and delete tags (#13731) 2025-12-08 10:15:34 +00:00
Henry Heino
16169b2780 Server: Periodically delete old backups for archived accounts (#13741) 2025-12-08 10:15:18 +00:00
Charlie Arehart
49ed4ae920 Docs: add links to clarify "plugins" references (#13763)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-12-08 10:11:26 +00:00
mrjo118
13777d261c Desktop: Replace the edit profile config menu option with a gui to manage profiles (#13771) 2025-12-08 10:08:58 +00:00
Henry Heino
1c7b0e6266 Mac: Fixes #13214: Markdown editor: Don't open links on ctrl-click (#13792) 2025-12-08 10:07:41 +00:00
Henry Heino
4589670126 Chore: Server: Debug: Add debug benchmarkDeltaPerformance API (#13801) 2025-12-08 10:07:25 +00:00
marph91
b6ab6e0b46 Desktop: Use the "--no-sandbox" flag for Tuxedo OS (#13810) 2025-12-08 10:06:42 +00:00
mrjo118
9b28b618bb Desktop, Mobile: Do no re-use the 'Restored Notes' folder if it is trashed (#13813) 2025-12-08 10:06:25 +00:00
Bartolomeo
bf7cc6be03 Desktop: Resolves: #13804: Change search Resources feature to case insensitive (#13824) 2025-12-08 10:04:31 +00:00
mrjo118
e5e5b342a7 Mobile: Fixes #13825: Fix incompatible plugins cannot be uninstalled (#13828) 2025-12-08 10:03:23 +00:00
Henry Heino
9709721a73 Desktop: OneNote importer: Fix missing content in imported notebooks, improve math formula import (#13829) 2025-12-08 09:59:58 +00:00
Henry Heino
a34010ef62 Chore: Tests: Make renderBlockImages.test.ts less likely to fail in CI (#13835) 2025-12-08 09:59:50 +00:00
Henry Heino
9a6043e6a6 Chore: Desktop: Migrate the "Share note" dialog to RSCSS (#13842) 2025-12-08 09:53:15 +00:00
Henry Heino
992bf683c4 Chore: Server: Create more realistic test data (#13843) 2025-12-08 09:53:05 +00:00
Henry Heino
b40c2b8a41 Desktop: Fixes #13844: OneNote importer: Fix wrong page version imported (#13850) 2025-12-08 09:52:51 +00:00
Henry Heino
8dcd08e21d Server: Ensure that shared items are processed in the correct order (#13858) 2025-12-08 09:52:39 +00:00
mrjo118
cb2b32520d Mobile: Prevent opening the edit / delete dialog when long pressing the conflicts notebook (#13860) 2025-12-08 09:51:12 +00:00
Henry Heino
315b1d8275 Desktop: Markdown Editor: Collapse selection to a single cursor when pressing "escape" (#13864) 2025-12-08 09:49:50 +00:00
renovate[bot]
8018f1269a Update dependency react-native-share to v12.2.0 (#13861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 10:17:21 +00:00
renovate[bot]
c2d186188b Update dependency react-native-safe-area-context to v5.6.1 (#13868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 11:05:56 +00:00
renovate[bot]
d5798e558b Update dependency react-native-safe-area-context to v5.6.0 (#13865)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 09:37:17 +00:00
renovate[bot]
224bcd54f1 Update dependency fs-extra to v11.3.1 (#13849)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 23:45:31 +00:00
Laurent Cozic
1a3d572498 Server v3.5.1 2025-12-03 11:56:50 +00:00
Laurent Cozic
848a2c986a Chore: Remove the need for yarn when bumping version number
Since "yarn version patch" also performs "yarn install" which is usually unnecessary
2025-12-03 11:56:04 +00:00
renovate[bot]
fc61a2bc6a Update dependency raw-body to v3.0.1 (#13846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 06:55:14 +00:00
Henry Heino
f9d58742c0 Desktop: Support converting multiple notes from HTML to Markdown at once (#13802) 2025-12-01 18:52:01 +00:00
renovate[bot]
5ba8cefe7c Update dependency nodejs to v24.4.1 (#13833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:51:35 +00:00
renovate[bot]
74484f194e Update Node.js to v24 (#13834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:51:30 +00:00
renovate[bot]
eae569aff8 Update dependency lint-staged to v16.1.6 (#13831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 13:58:57 +00:00
renovate[bot]
8734bc8467 Update dependency nodejs to v24 (#13832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 13:58:50 +00:00
renovate[bot]
612d09d16f Update dependency lint-staged to v16.1.2 (#13822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 12:07:32 +00:00
Joplin Bot
eb2e9419b9 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-12-01 02:04:54 +00:00
156 changed files with 3028 additions and 1197 deletions

View File

@@ -165,8 +165,6 @@ packages/app-desktop/app.reducer.js
packages/app-desktop/app.js
packages/app-desktop/bridge.js
packages/app-desktop/checkForUpdates.js
packages/app-desktop/commands/convertNoteToMarkdown.test.js
packages/app-desktop/commands/convertNoteToMarkdown.js
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyToClipboard.js
packages/app-desktop/commands/editProfileConfig.js
@@ -183,6 +181,7 @@ packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/showProfileEditor.js
packages/app-desktop/commands/startExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/switchProfile.js
@@ -207,7 +206,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
packages/app-desktop/gui/ConversionNotification/ConversionNotification.js
packages/app-desktop/gui/Dialog.js
packages/app-desktop/gui/DialogButtonRow.js
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
@@ -393,6 +391,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/ProfileEditor.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@@ -1031,6 +1030,7 @@ packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
packages/editor/CodeMirror/extensions/editorSettingsExtension.js
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
@@ -1245,6 +1245,8 @@ packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/convertHtmlToMarkdown.test.js
packages/lib/commands/convertHtmlToMarkdown.js
packages/lib/commands/convertNoteToMarkdown.test.js
packages/lib/commands/convertNoteToMarkdown.js
packages/lib/commands/deleteNote.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
@@ -1854,6 +1856,7 @@ packages/tools/generate-database-types.js
packages/tools/generate-images.js
packages/tools/git-changelog.test.js
packages/tools/git-changelog.js
packages/tools/licenses/buildReport.js
packages/tools/licenses/getLicenses.js
packages/tools/licenses/licenseChecker.js
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js

9
.gitignore vendored
View File

@@ -137,8 +137,6 @@ packages/app-desktop/app.reducer.js
packages/app-desktop/app.js
packages/app-desktop/bridge.js
packages/app-desktop/checkForUpdates.js
packages/app-desktop/commands/convertNoteToMarkdown.test.js
packages/app-desktop/commands/convertNoteToMarkdown.js
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyToClipboard.js
packages/app-desktop/commands/editProfileConfig.js
@@ -155,6 +153,7 @@ packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/showProfileEditor.js
packages/app-desktop/commands/startExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/switchProfile.js
@@ -179,7 +178,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
packages/app-desktop/gui/ConversionNotification/ConversionNotification.js
packages/app-desktop/gui/Dialog.js
packages/app-desktop/gui/DialogButtonRow.js
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
@@ -365,6 +363,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/ProfileEditor.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@@ -1003,6 +1002,7 @@ packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
packages/editor/CodeMirror/extensions/editorSettingsExtension.js
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
@@ -1217,6 +1217,8 @@ packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/convertHtmlToMarkdown.test.js
packages/lib/commands/convertHtmlToMarkdown.js
packages/lib/commands/convertNoteToMarkdown.test.js
packages/lib/commands/convertNoteToMarkdown.js
packages/lib/commands/deleteNote.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
@@ -1826,6 +1828,7 @@ packages/tools/generate-database-types.js
packages/tools/generate-images.js
packages/tools/git-changelog.test.js
packages/tools/git-changelog.js
packages/tools/licenses/buildReport.js
packages/tools/licenses/getLicenses.js
packages/tools/licenses/licenseChecker.js
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,4 +1,4 @@
FROM node:18-bullseye
FROM node:24-bullseye
RUN apt-get update \
&& apt-get install -y \

View File

@@ -198,12 +198,21 @@ else
fi
# Check if it's in the latest version
if [[ -e "${INSTALL_DIR}/VERSION" ]] && [[ $(< "${INSTALL_DIR}/VERSION") == "${RELEASE_VERSION}" ]]; then
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
if [[ -e "${INSTALL_DIR}/VERSION" ]]; then
CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
VERSION_COMPARISON=$(compareVersions "$CURRENT_VERSION" "$RELEASE_VERSION")
if [[ "$VERSION_COMPARISON" == "0" ]]; then
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
elif [[ "$VERSION_COMPARISON" == "1" ]]; then
print "${COLOR_YELLOW}You have version ${CURRENT_VERSION} installed, which is newer than the latest published version ${RELEASE_VERSION}.${COLOR_RESET}"
print "${COLOR_YELLOW}Skipping installation to avoid downgrade.${COLOR_RESET}"
else
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION} installed."
fi
else
[[ -e "${INSTALL_DIR}/VERSION" ]] && CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION:-no version} installed."
print "The latest version is ${RELEASE_VERSION}, but you have no version installed."
fi
# Check if it's an update or a new install
@@ -275,7 +284,7 @@ if command -v lsb_release &> /dev/null; then
# without writing the AppImage to a non-user-writable location (without invalidating other security
# controls). See https://discourse.joplinapp.org/t/possible-future-requirement-for-no-sandbox-flag-for-ubuntu-23-10/.
HAS_USERNS_RESTRICTIONS=false
if [[ "$DISTVER" =~ ^Ubuntu && $DISTMAJOR -ge 23 ]]; then
if [[ "$DISTVER" =~ ^(Ubuntu|Tuxedo) && $DISTMAJOR -ge 23 ]]; then
HAS_USERNS_RESTRICTIONS=true
fi

View File

@@ -9,7 +9,7 @@
"vips.dev": {
"platforms": ["aarch64-darwin"],
},
"nodejs": "23.11.0",
"nodejs": "24.5.0",
"pkg-config": "latest",
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix

View File

@@ -81,12 +81,12 @@
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-react": "7.37.5",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"fs-extra": "11.3.1",
"glob": "11.0.3",
"gulp": "4.0.2",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "16.0.0",
"lint-staged": "16.1.6",
"madge": "8.0.0",
"npm-package-json-lint": "8.0.0",
"typescript": "5.8.3"

View File

@@ -2,6 +2,7 @@ import app from '../app';
import Folder from '@joplin/lib/models/Folder';
import BaseCommand from '../base-command';
import setupCommand from '../setupCommand';
import Setting from '@joplin/lib/models/Setting';
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
export const setupCommandForTesting = (CommandClass: any, stdout: Function = null): BaseCommand => {
@@ -18,4 +19,9 @@ export const setupApplication = async () => {
// Some tests also need access to the Redux store
app().initRedux();
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTING_UPDATE_ALL event, which means state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
};

View File

@@ -48,7 +48,7 @@
"chalk": "4.1.2",
"compare-version": "0.1.2",
"file-type": "16.5.4",
"fs-extra": "11.2.0",
"fs-extra": "11.3.1",
"html-entities": "1.4.0",
"keytar": "7.9.0",
"md5": "2.3.0",

View File

@@ -0,0 +1,11 @@
<ul>
<li><a href="https://example.com/" title="This
is a test title
testing!
Test...">Test!</a></li>
<li><a href="http://example.com" title="
Test
">Another test...</a></li>
</ul>

View File

@@ -0,0 +1,5 @@
- [Test!](https://example.com/ "This
is a test title
testing!
Test...")
- [Another test...](http://example.com "Test")

Binary file not shown.

View File

@@ -0,0 +1,3 @@
# test for joplin import
[https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm\_source%3DYouTube%2520Instagram%26utm\_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm\_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm\_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm\_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm\_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o\_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&\_\_tn\_\_=H-y-R&c[0]=AT0eE6OXx\_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887\_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG\_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo76663333hhsgsu](<https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm_source%3DYouTube%2520Instagram%26utm_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&__tn__=H-y-R&c[0]=AT0eE6OXx_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo>)

View File

@@ -0,0 +1,9 @@
---
id: 20250821081408
date: 2025-08-21
keywords:
---
# A test file for Joplin importer
Test

View File

@@ -0,0 +1,7 @@
---
title: test
created: 2025-07-22 17:30:44Z
updated: 2025-07-22 17:37:48Z
---
test

View File

@@ -1,7 +1,7 @@
---
title: "Frontmatter test"
created_at: 01-01-2024 01:23 AM
updated_at: 02-01-2024 04:56 AM
updated_at: 01-01-2024 04:56 AM
---
# Frontmatter test

View File

@@ -281,14 +281,16 @@ class Application extends BaseApplication {
}
// As of Joplin 3.5.7, the ABC rendering is part of the app so we automatically disable the plugin
pluginSettings = {
...pluginSettings,
['org.joplinapp.plugins.AbcSheetMusic']: {
enabled: false,
deleted: false,
hasBeenUpdated: false,
},
};
if (pluginSettings['org.joplinapp.plugins.AbcSheetMusic']) {
pluginSettings = {
...pluginSettings,
['org.joplinapp.plugins.AbcSheetMusic']: {
enabled: false,
deleted: false,
hasBeenUpdated: false,
},
};
}
try {
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {

View File

@@ -1,52 +0,0 @@
import { _ } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import { stateUtils } from '@joplin/lib/reducer';
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { MarkupLanguage } from '@joplin/renderer';
import { runtime as convertHtmlToMarkdown } from '@joplin/lib/commands/convertHtmlToMarkdown';
import bridge from '../services/bridge';
export const declaration: CommandDeclaration = {
name: 'convertNoteToMarkdown',
label: () => _('Convert note to Markdown'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, noteId: string = null) => {
noteId = noteId || stateUtils.selectedNoteId(context.state);
const note = await Note.load(noteId);
if (!note) return;
try {
const markdownBody = await convertHtmlToMarkdown().execute(context, note.body);
const newNote = await Note.duplicate(note.id);
newNote.body = markdownBody;
newNote.markup_language = MarkupLanguage.Markdown;
await Note.save(newNote);
await Note.delete(note.id, { toTrash: true });
context.dispatch({
type: 'NOTE_HTML_TO_MARKDOWN_DONE',
value: note.id,
});
context.dispatch({
type: 'NOTE_SELECT',
id: newNote.id,
});
} catch (error) {
bridge().showErrorMessageBox(_('Could not convert note to Markdown: %s', error.message));
}
},
enabledCondition: 'oneNoteSelected && noteIsHtml && !noteIsReadOnly',
};
};

View File

@@ -1,5 +1,4 @@
// AUTO-GENERATED using `gulp buildScriptIndexes`
import * as convertNoteToMarkdown from './convertNoteToMarkdown';
import * as copyDevCommand from './copyDevCommand';
import * as copyToClipboard from './copyToClipboard';
import * as editProfileConfig from './editProfileConfig';
@@ -14,6 +13,7 @@ import * as openProfileDirectory from './openProfileDirectory';
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as showProfileEditor from './showProfileEditor';
import * as startExternalEditing from './startExternalEditing';
import * as stopExternalEditing from './stopExternalEditing';
import * as switchProfile from './switchProfile';
@@ -25,7 +25,6 @@ import * as toggleSafeMode from './toggleSafeMode';
import * as toggleTabMovesFocus from './toggleTabMovesFocus';
const index: any[] = [
convertNoteToMarkdown,
copyDevCommand,
copyToClipboard,
editProfileConfig,
@@ -40,6 +39,7 @@ const index: any[] = [
openSecondaryAppInstance,
replaceMisspelling,
restoreNoteRevision,
showProfileEditor,
startExternalEditing,
stopExternalEditing,
switchProfile,

View File

@@ -0,0 +1,20 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'showProfileEditor',
label: () => _('Manage profiles'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
context.dispatch({
type: 'NAV_GO',
routeName: 'ProfileEditor',
});
},
enabledCondition: 'hasMultiProfiles',
};
};

View File

@@ -1,28 +0,0 @@
import * as React from 'react';
import { useContext, useEffect } from 'react';
import { _ } from '@joplin/lib/locale';
import { Dispatch } from 'redux';
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
import { NotificationType } from '../PopupNotification/types';
interface Props {
noteId: string;
dispatch: Dispatch;
}
export default (props: Props) => {
const popupManager = useContext(PopupNotificationContext);
useEffect(() => {
if (!props.noteId || props.noteId === '') return;
props.dispatch({ type: 'NOTE_HTML_TO_MARKDOWN_DONE', value: '' });
const notification = popupManager.createPopup(() => (
<div>{_('The note has been converted to Markdown and the original note has been moved to the trash')}</div>
), { type: NotificationType.Success });
notification.scheduleDismiss();
}, [props.dispatch, popupManager, props.noteId]);
return <div style={{ display: 'none' }}/>;
};

View File

@@ -38,14 +38,12 @@ import restart from '../services/restart';
import { connect } from 'react-redux';
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
import validateColumns from './NoteListHeader/utils/validateColumns';
import ConversionNotification from './ConversionNotification/ConversionNotification';
import TrashNotification from './TrashNotification/TrashNotification';
import UpdateNotification from './UpdateNotification/UpdateNotification';
import NoteEditor from './NoteEditor/NoteEditor';
import PluginNotification from './PluginNotification/PluginNotification';
import { Toast } from '@joplin/lib/services/plugins/api/types';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import { Dispatch } from 'redux';
const ipcRenderer = require('electron').ipcRenderer;
@@ -86,7 +84,6 @@ interface Props {
showInvalidJoplinCloudCredential: boolean;
toast: Toast;
shouldSwitchToAppleSiliconVersion: boolean;
noteHtmlToMarkdownDone: string;
}
interface ShareFolderDialogOptions {
@@ -800,10 +797,6 @@ class MainScreenComponent extends React.Component<Props, State> {
return (
<div style={style}>
<ConversionNotification
noteId={this.props.noteHtmlToMarkdownDone}
dispatch={this.props.dispatch as Dispatch}
/>
<TrashNotification
lastDeletion={this.props.lastDeletion}
lastDeletionNotificationTime={this.props.lastDeletionNotificationTime}
@@ -859,8 +852,7 @@ const mapStateToProps = (state: AppState) => {
notesColumns: validateColumns(state.settings['notes.columns']),
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
toast: state.toast,
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && process.arch !== 'arm64',
noteHtmlToMarkdownDone: state.noteHtmlToMarkdownDone,
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && shim.isMac() && process.arch !== 'arm64',
};
};

View File

@@ -117,7 +117,7 @@ const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: an
switchProfileMenuItems.push({ type: 'separator' });
switchProfileMenuItems.push(menuItemDic.addProfile);
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
switchProfileMenuItems.push(menuItemDic.showProfileEditor);
return switchProfileMenuItems;
}, [profileConfig, menuItemDic]);

View File

@@ -31,13 +31,15 @@ function markupToHtml() {
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, lineSetter: Function) {
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, cjkCharacterSetter: React.Dispatch<React.SetStateAction<number>>, lineSetter: Function) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
Countable.count(text, (counter: any) => {
wordSetter(counter.words);
characterSetter(counter.all);
characterNoSpaceSetter(counter.characters);
});
const cjkMatches = text.match(/[\p{Script=Han}\p{Script=Bopomofo}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu);
cjkCharacterSetter(cjkMatches ? cjkMatches.length : 0);
lineSetter(text === '' ? 0 : text.split('\n').length);
}
@@ -58,23 +60,25 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
const [words, setWords] = useState<number>(0);
const [characters, setCharacters] = useState<number>(0);
const [charactersNoSpace, setCharactersNoSpace] = useState<number>(0);
const [cjkCharacters, setCjkCharacters] = useState<number>(0);
// For source with Markdown syntax stripped out
const [strippedLines, setStrippedLines] = useState<number>(0);
const [strippedWords, setStrippedWords] = useState<number>(0);
const [strippedCharacters, setStrippedCharacters] = useState<number>(0);
const [strippedCharactersNoSpace, setStrippedCharactersNoSpace] = useState<number>(0);
const [strippedCjkCharacters, setStrippedCjkCharacters] = useState<number>(0);
const [strippedReadTime, setStrippedReadTime] = useState<number>(0);
// This amount based on the following paper:
// https://www.researchgate.net/publication/332380784_How_many_words_do_we_read_per_minute_A_review_and_meta-analysis_of_reading_rate
const wordsPerMinute = 250;
useEffect(() => {
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setLines);
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setCjkCharacters, setLines);
}, [props.text]);
useEffect(() => {
const strippedText: string = markupToHtml().stripMarkup(props.markupLanguage, props.text);
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedLines);
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedCjkCharacters, setStrippedLines);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.text]);
@@ -88,6 +92,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
words: words,
characters: characters,
charactersNoSpace: charactersNoSpace,
cjkCharacters: cjkCharacters,
};
const strippedTextProperties: TextPropertiesMap = {
@@ -99,12 +104,14 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
words: strippedWords,
characters: strippedCharacters,
charactersNoSpace: strippedCharactersNoSpace,
cjkCharacters: strippedCjkCharacters,
};
const keyToLabel: KeyToLabelMap = {
words: _('Words'),
characters: _('Characters'),
charactersNoSpace: _('Characters excluding spaces'),
cjkCharacters: _('Chinese/Japanese/Korean characters'),
lines: _('Lines'),
};
@@ -147,6 +154,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
);
for (const key in textProperties) {
if (key === 'cjkCharacters' && textProperties[key] === 0 && strippedTextProperties[key] === 0) continue;
const comp = createTableBodyRow(key, textProperties[key], strippedTextProperties[key]);
tableBodyComps.push(comp);
}

View File

@@ -366,6 +366,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
ignoreModifiers: true,
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
keymap: keyboardMode,
preferMacShortcuts: shim.isMac(),
indentWithTabs: true,
tabMovesFocus: props.tabMovesFocus,
editorLabel: _('Markdown editor'),

View File

@@ -1051,6 +1051,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
editor,
});
const noteChangeTimeRef = useRef(Date.now());
const lastNoteIdRef = useRef(props.noteId);
useEffect(() => {
if (!editor) return () => {};
@@ -1068,6 +1069,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// Use nextOnChangeEventInfo's noteId -- lastOnChangeEventInfo can be slightly out-of-date.
const differentNoteId = lastNoteIdRef.current !== props.noteId;
const differentContent = lastOnChangeEventInfo.current.content !== props.content;
if (differentNoteId) noteChangeTimeRef.current = Date.now();
if (differentNoteId || differentContent || !resourcesEqual) {
const result = await props.markupToHtml(
props.contentMarkupLanguage,
@@ -1340,7 +1344,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// keep it this way for now.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function onKeyUp(event: any) {
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key)) {
const timeSinceNoteChange = Date.now() - noteChangeTimeRef.current;
// A key that is pressed before the editor is opened, and that is released after it is
// opened is going to be processed here. For example if the user presses Enter in
// GotoAnything to arrive here. But in that case, we don't want the change handler to be
// activated, because that would change the note timestamp. So we take into account how
// long the note has been loaded before we process the key. Fixes
// https://github.com/laurent22/joplin/issues/12367
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key) && timeSinceNoteChange > 200) {
onChangeHandler();
}
}

View File

@@ -1,7 +1,8 @@
import * as React from 'react';
import { createContext, useMemo, useRef, useState } from 'react';
import { createContext, useEffect, useMemo, useRef, useState } from 'react';
import { NotificationType, PopupHandle, PopupControl as PopupManager } from './types';
import { Hour, msleep } from '@joplin/utils/time';
import shim from '@joplin/lib/shim';
export const PopupNotificationContext = createContext<PopupManager|null>(null);
export const VisibleNotificationsContext = createContext<PopupSpec[]>([]);
@@ -112,6 +113,18 @@ const PopupNotificationProvider: React.FC<Props> = props => {
return manager;
}, []);
useEffect(() => {
const defaultShowToast = shim.showToast;
shim.showToast = async (message: string, options) => {
const popup = popupManager.createPopup(() => message, { type: options?.type ?? NotificationType.Info });
popup.scheduleDismiss();
};
return () => {
shim.showToast = defaultShowToast;
};
}, [popupManager]);
return <PopupNotificationContext.Provider value={popupManager}>
<VisibleNotificationsContext.Provider value={popupSpecs}>
{props.children}

View File

@@ -1,3 +1,4 @@
import { ToastType } from '@joplin/lib/shim';
import * as React from 'react';
export type PopupHandle = {
@@ -5,14 +6,13 @@ export type PopupHandle = {
scheduleDismiss(delay?: number): void;
};
export enum NotificationType {
Info = 'info',
Success = 'success',
Error = 'error',
}
export type NotificationContentCallback = ()=> React.ReactNode;
// NotificationType is an alias for ToastType
export type NotificationType = ToastType;
// eslint-disable-next-line no-redeclare -- export const is necessary for creating an alias, this is not a redeclaration.
export const NotificationType = ToastType;
export interface PopupOptions {
type?: NotificationType;
}

View File

@@ -0,0 +1,47 @@
.profile-management {
font-family: var(--joplin-font-family);
display: flex;
flex-direction: column;
> .tableContainer {
overflow-y: scroll;
padding: 20px;
box-sizing: border-box;
flex: 1 1 0%;
color: var(--joplin-color);
> .notification {
margin-bottom: 10px;
}
}
}
.profile-table {
width: 100%;
> thead > tr > .headerCell {
white-space: nowrap;
font-weight: bold;
width: 1px;
}
> tbody > tr {
> .nameCell {
text-overflow: ellipsis;
overflow-x: hidden;
max-width: 1px;
width: 100%;
white-space: nowrap;
}
> .dataCell {
white-space: nowrap;
width: 1px;
color: var(--joplin-color-faded);
}
> .profileActions > button {
margin-right: 10px;
}
}
}

View File

@@ -0,0 +1,193 @@
import * as React from 'react';
import { useState, useEffect, CSSProperties } from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
import { connect } from 'react-redux';
import { themeStyle } from '@joplin/lib/theme';
import bridge from '../services/bridge';
import dialogs from './dialogs';
import { Profile, ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import { deleteProfileById, saveProfileConfig } from '@joplin/lib/services/profileConfig';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import Logger from '@joplin/utils/Logger';
import { AppState } from '../app.reducer';
import { Dispatch } from 'redux';
const logger = Logger.create('ProfileEditor');
interface Props {
themeId: number;
dispatch: Dispatch;
style: CSSProperties;
profileConfig: ProfileConfig;
}
interface ProfileTableProps {
profiles: Profile[];
currentProfileId: string;
onProfileRename: (profile: Profile)=> void;
onProfileDelete: (profile: Profile)=> void;
themeId: number;
}
const ProfileTableComp: React.FC<ProfileTableProps> = props => {
const theme = themeStyle(props.themeId);
return (
<table className="profile-table">
<thead>
<tr>
<th className="headerCell">{_('Profile name')}</th>
<th className="headerCell">{_('ID')}</th>
<th className="headerCell">{_('Status')}</th>
<th className="headerCell">{_('Actions')}</th>
</tr>
</thead>
<tbody>
{props.profiles.map((profile: Profile, index: number) => {
const isCurrentProfile = profile.id === props.currentProfileId;
return (
<tr key={index}>
<td id={`name-${profile.id}`} className="nameCell">
<span style={{ fontWeight: isCurrentProfile ? 'bold' : 'normal' }}>
{profile.name || `(${_('Untitled')})`}
</span>
</td>
<td className="dataCell">{profile.id}</td>
<td className="dataCell">
{isCurrentProfile ? _('Active') : ''}
</td>
<td className="dataCell profileActions">
<button
id={`rename-${profile.id}`}
aria-labelledby={`rename-${profile.id} name-${profile.id}`}
style={theme.buttonStyle}
onClick={() => props.onProfileRename(profile)}
>
{_('Rename')}
</button>
{!isCurrentProfile && (
<button
id={`delete-${profile.id}`}
aria-labelledby={`delete-${profile.id} name-${profile.id}`}
style={theme.buttonStyle}
onClick={() => props.onProfileDelete(profile)}
>
{_('Delete')}
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
};
const ProfileEditorComponent: React.FC<Props> = props => {
const { profileConfig, themeId, dispatch } = props;
const theme = themeStyle(themeId);
const style = props.style;
const containerHeight = style.height;
const [profiles, setProfiles] = useState<Profile[]>(profileConfig.profiles);
useEffect(() => {
setProfiles(profileConfig.profiles);
}, [profileConfig]);
const saveNewProfileConfig = async (makeNewProfileConfig: ()=> ProfileConfig) => {
try {
const newProfileConfig = makeNewProfileConfig();
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newProfileConfig);
dispatch({
type: 'PROFILE_CONFIG_SET',
value: newProfileConfig,
});
} catch (error) {
logger.error(error);
bridge().showErrorMessageBox(error.message);
}
};
const onProfileRename = async (profile: Profile) => {
const newName = await dialogs.prompt(_('Profile name:'), '', profile.name);
if (newName === null || newName === undefined || newName === profile.name) return;
if (!newName.trim()) {
bridge().showErrorMessageBox(_('Profile name cannot be empty'));
return;
}
const makeNewProfileConfig = () => {
const newProfiles = profileConfig.profiles.map(p =>
p.id === profile.id ? { ...p, name: newName.trim() } : p,
);
const newProfileConfig = {
...profileConfig,
profiles: newProfiles,
};
return newProfileConfig;
};
await saveNewProfileConfig(makeNewProfileConfig);
};
const onProfileDelete = async (profile: Profile) => {
const isCurrentProfile = profile.id === profileConfig.currentProfileId;
if (isCurrentProfile) {
bridge().showErrorMessageBox(_('The active profile cannot be deleted. Switch to a different profile and try again.'));
return;
}
const ok = bridge().showConfirmMessageBox(_('Delete profile "%s"?\n\nAll data, including notes, notebooks and tags will be permanently deleted.', profile.name), {
buttons: [_('Delete'), _('Cancel')],
defaultId: 1,
});
if (!ok) return;
const rootDir = Setting.value('rootProfileDir');
const profileDir = `${rootDir}/profile-${profile.id}`;
try {
await shim.fsDriver().remove(profileDir);
logger.info('Deleted profile directory: ', profileDir);
} catch (error) {
logger.error('Error deleting profile directory: ', error);
bridge().showErrorMessageBox(error.message);
}
await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id));
};
return (
<div className="profile-management" style={{ ...theme.containerStyle, height: containerHeight }}>
<div className="tableContainer">
<div className="notification" style={theme.notificationBox}>
{_('Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.')}
</div>
<ProfileTableComp
themeId={themeId}
profiles={profiles}
currentProfileId={profileConfig.currentProfileId}
onProfileRename={onProfileRename}
onProfileDelete={onProfileDelete}
/>
</div>
<ButtonBar
onCancelClick={() => dispatch({ type: 'NAV_BACK' })}
/>
</div>
);
};
const mapStateToProps = (state: AppState) => ({
themeId: state.settings.theme,
profileConfig: state.profileConfig,
});
export default connect(mapStateToProps)(ProfileEditorComponent);

View File

@@ -84,7 +84,13 @@ const ResourceTableComp = (props: ResourceTable) => {
};
const filteredResources = props.resources.filter(
(resource: InnerResource) => !props.filter || resource.title?.includes(props.filter) || resource.id.includes(props.filter),
(resource: InnerResource) => {
if (props.filter) {
const filterLowerCase = props.filter.toLowerCase();
return resource.title?.toLowerCase().includes(filterLowerCase) || resource.id.toLowerCase().includes(filterLowerCase);
}
return true;
},
);
const renderSortableHeader = (title: string, order: SortingOrder) => {
@@ -297,7 +303,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
<div style={{ ...theme.notificationBox, marginBottom: 10 }}>{
_('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.')
}</div>
<div style={{ float: 'right' }}>
<p style={{ float: 'left', paddingRight: 10 }}>
<input
style={theme.inputStyle}
type="search"
@@ -305,7 +311,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
onChange={this.onFilterUpdate}
placeholder={_('Search...')}
/>
</div>
</p>
{this.state.isLoading && <div>{_('Please wait...')}</div>}
{!this.state.isLoading && <div>
{!this.state.resources && <div>

View File

@@ -20,6 +20,7 @@ import Dialog from './Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
import ImportScreen from './ImportScreen';
import ResourceScreen from './ResourceScreen';
import ProfileEditor from './ProfileEditor';
import Navigator from './Navigator';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
@@ -165,6 +166,7 @@ class RootComponent extends React.Component<Props, any> {
Import: { screen: ImportScreen, title: () => _('Import') },
Config: { screen: ConfigScreen, title: () => _('Options') },
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
ProfileEditor: { screen: ProfileEditor, title: () => _('Manage profiles') },
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
};

View File

@@ -3,7 +3,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import DialogButtonRow from './DialogButtonRow';
import { themeStyle, buildStyle } from '@joplin/lib/theme';
import Dialog from './Dialog';
import DialogTitle from './DialogTitle';
import ShareService from '@joplin/lib/services/share/ShareService';
@@ -29,47 +28,6 @@ interface Props {
syncTargetId: number;
}
function styles_(props: Props) {
return buildStyle('ShareNoteDialog', props.themeId, theme => {
return {
root: {
minWidth: 500,
},
noteList: {
marginBottom: 10,
},
note: {
flex: 1,
flexDirection: 'row',
display: 'flex',
alignItems: 'center',
border: '1px solid',
borderColor: theme.dividerColor,
padding: '0.5em',
marginBottom: 5,
},
noteTitle: {
...theme.textStyle,
flex: 1,
display: 'flex',
color: theme.color,
},
noteRemoveButton: {
background: 'none',
border: 'none',
},
noteRemoveButtonIcon: {
color: theme.color,
fontSize: '1.4em',
},
copyShareLinkButton: {
...theme.buttonStyle,
marginBottom: 10,
},
};
});
}
export function ShareNoteDialog(props: Props) {
const [notes, setNotes] = useState<NoteEntity[]>([]);
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
@@ -77,8 +35,6 @@ export function ShareNoteDialog(props: Props) {
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
const noteCount = notes.length;
const theme = themeStyle(props.themeId);
const styles = styles_(props);
useEffect(() => {
void ShareService.instance().refreshShares();
@@ -117,8 +73,8 @@ export function ShareNoteDialog(props: Props) {
);
return (
<div key={note.id} style={styles.note}>
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
<div key={note.id} className='shared-note-list-item'>
<span className='title'>{note.title}</span>{unshareButton}
</div>
);
};
@@ -128,7 +84,7 @@ export function ShareNoteDialog(props: Props) {
for (const note of notes) {
noteComps.push(renderNote(note));
}
return <div style={styles.noteList}>{noteComps}</div>;
return <div className="notes">{noteComps}</div>;
};
const statusMessage = useShareStatusMessage({ sharesState, noteCount });
@@ -136,7 +92,7 @@ export function ShareNoteDialog(props: Props) {
function renderEncryptionWarningMessage() {
if (!encryptionWarning) return null;
return <div style={theme.textStyle}>{encryptionWarning}<hr/></div>;
return <div className="message">{encryptionWarning}<hr/></div>;
}
const onRecursiveShareChange = useCallback(() => {
@@ -155,12 +111,16 @@ export function ShareNoteDialog(props: Props) {
const renderContent = () => {
return (
<div style={styles.root} className="form">
<div className="form share-note-dialog">
<DialogTitle title={_('Publish Notes')}/>
{renderNoteList(notes)}
{renderRecursiveShareCheckbox()}
<button disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage}</div>
<button
disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0}
className="share"
onClick={shareLinkButton_click}
>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div className="message">{statusMessage}</div>
{renderEncryptionWarningMessage()}
<DialogButtonRow
themeId={props.themeId}

View File

@@ -10,8 +10,11 @@ import Setting from '@joplin/lib/models/Setting';
import { PackageInfo } from '@joplin/lib/versionInfo';
import shim from '@joplin/lib/shim';
import { ImportModule } from '@joplin/lib/services/interop/Module';
import Logger from '@joplin/utils/Logger';
const packageInfo: PackageInfo = require('../../../packageInfo.js');
const logger = Logger.create('importFrom');
export const declaration: CommandDeclaration = {
name: 'importFrom',
label: () => _('Import...'),
@@ -135,6 +138,7 @@ export const runtime = (control: WindowControl): CommandRuntime => {
// eslint-disable-next-line no-console
console.info('Import result: ', result);
} catch (error) {
logger.error(error);
bridge().showErrorMessageBox(error.message);
}

View File

@@ -73,7 +73,7 @@ export default function() {
'commandPalette',
'openMasterPasswordDialog',
'addProfile',
'editProfileConfig',
'showProfileEditor',
'switchProfile1',
'switchProfile2',
'switchProfile3',

View File

@@ -0,0 +1,16 @@
// Corresponds to theme.buttonStyle
.base-button {
border: 1px solid;
min-height: 26px;
min-width: 80px;
padding-left: 12px;
padding-right: 12px;
padding-top: 6px;
padding-bottom: 6px;
border-radius: 4px;
color: var(--joplin-color4);
background-color: var(--joplin-background-color4);
border-color: var(--joplin-border-color4);
user-select: none;
}

View File

@@ -0,0 +1,8 @@
// Corresponds to theme.textStyle
.base-text {
font-family: var(--joplin-font-family);
font-size: var(--joplin-font-size);
line-height: 1.6em;
color: var(--joplin-color);
}

View File

@@ -1,4 +1,6 @@
@use './base-text.scss';
@use './base-button.scss';
@use './dialog-modal-layer.scss';
@use './user-webview-dialog.scss';
@use './prompt-dialog.scss';
@@ -19,3 +21,5 @@
@use './popup-notification-list.scss';
@use './popup-notification-item.scss';
@use './multi-note-actions.scss';
@use './share-note-dialog.scss';
@use './shared-note-list-item.scss';

View File

@@ -0,0 +1,19 @@
@use "./base-button.scss";
@use "./base-text.scss";
.share-note-dialog {
min-width: 500px;
> .message {
@extend .base-text;
}
> .notes {
margin-bottom: 10px;
}
> .share {
@extend .base-button;
margin-bottom: 10px;
}
}

View File

@@ -0,0 +1,20 @@
@use "./base-text.scss";
.shared-note-list-item {
flex: 1;
flex-direction: row;
display: flex;
align-items: center;
border: 1px solid;
border-color: var(--joplin-divider-color);
padding: 0.5em;
margin-bottom: 5px;
> .title {
@extend .base-text;
flex: 1;
display: flex;
color: var(--joplin-color);
}
}

View File

@@ -13,6 +13,7 @@ import Setting from '@joplin/lib/models/Setting';
const { clipboard } = require('electron');
import { Dispatch } from 'redux';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { MarkupLanguage } from '@joplin/renderer';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@@ -143,6 +144,16 @@ export default class NoteListUtils {
menu.append(new MenuItem({ type: 'separator' }));
const includesHtmlNotes = notes.some(n => n.markup_language === MarkupLanguage.Html);
if (includesHtmlNotes) {
menu.append(
new MenuItem(
menuUtils.commandToStatefulMenuItem('convertNoteToMarkdown', noteIds),
),
);
menu.append(new MenuItem({ type: 'separator' }));
}
if ([9, 10, 11].includes(Setting.value('sync.target'))) {
menu.append(
new MenuItem(
@@ -204,7 +215,6 @@ export default class NoteListUtils {
);
}
const pluginViewInfos = pluginUtils.viewInfosByType(props.plugins, 'menuItem');
for (const info of pluginViewInfos) {

View File

@@ -209,7 +209,7 @@
"dependencies": {
"@electron/remote": "2.1.3",
"@joplin/onenote-converter": "~3.5",
"fs-extra": "11.2.0",
"fs-extra": "11.3.1",
"keytar": "7.9.0",
"node-fetch": "2.6.7",
"sqlite3": "5.1.6"

View File

@@ -13,6 +13,7 @@
@use 'gui/Sidebar/style.scss' as sidebar-styles;
@use 'gui/NoteEditor/style.scss' as note-editor-styles;
@use 'gui/KeymapConfig/style.scss' as keymap-styles;
@use 'gui/ProfileEditor.scss' as profile-editor;
@use 'services/plugins/styles/index.scss' as plugins-styles;
@use 'gui/styles/index.scss' as gui-styles;
@use 'main.scss' as main;

View File

@@ -89,8 +89,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097781
versionName "3.5.1"
versionCode 2097783
versionName "3.5.3"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -29,7 +29,7 @@ const useStyles = () => {
const TextInputDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
const styles = useStyles();
const [text, setText] = useState('');
const [text, setText] = useState(dialog.initialValue ?? '');
const labelId = useId();
return (

View File

@@ -91,7 +91,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
});
});
},
promptForText: (message: string) => {
promptForText: (message: string, initialValue?: string) => {
return new Promise<string|null>((resolve) => {
const dismiss = () => {
onDismiss(dialog);
@@ -101,6 +101,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
type: DialogType.TextInput,
key: `prompt-dialog-${nextDialogIdRef.current++}`,
message,
initialValue,
onSubmit: (text) => {
resolve(text);
dismiss();

View File

@@ -24,7 +24,7 @@ export interface DialogControl {
info(message: string): Promise<void>;
error(message: string): Promise<void>;
prompt(title: string, message: string, buttons?: PromptButtonSpec[], options?: PromptOptions): void;
promptForText(message: string): Promise<string>;
promptForText(message: string, initialValue?: string): Promise<string>;
showMenu<IdType>(title: string, choices: MenuChoice<IdType>[]): Promise<IdType>;
}
@@ -47,6 +47,7 @@ export interface TextInputDialogData {
type: DialogType.TextInput;
key: string;
message: string;
initialValue?: string;
onSubmit: (text: string)=> void;
onDismiss: ()=> void;
}

View File

@@ -274,6 +274,7 @@ function NoteEditor(props: Props) {
highlightActiveLine,
keymap: EditorKeymap.Default,
preferMacShortcuts: shim.mobilePlatform() === 'ios',
automatchBraces: false,
ignoreModifiers: false,

View File

@@ -11,7 +11,7 @@ export enum InstallState {
interface Props {
onPress: ()=> void;
disabled: boolean;
disabled?: boolean;
children: React.ReactNode;
style?: ViewStyle;
testID?: string;

View File

@@ -632,6 +632,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
if (Platform.OS !== 'ios') addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/');
addSettingLink('website_link', _('Joplin website'), 'https://joplinapp.org/');
addSettingLink('privacy_link', _('Privacy Policy'), 'https://joplinapp.org/privacy/');
addSettingLink('license_link', _('Open-source licences'), 'https://raw.githubusercontent.com/laurent22/joplin/refs/heads/dev/readme/licenses.md');
const versionInfoText = getVersionInfoText(settings['plugins.states']);

View File

@@ -70,7 +70,6 @@ const PluginBox: React.FC<Props> = props => {
style={styles.cardContainer}
onPress={props.onShowPluginInfo ? onPress : null}
testID='plugin-card'
disabled={!props.isCompatible}
>
<Card.Content style={styles.content}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity } from 'react-native';
import { View, Text, FlatList, StyleSheet, AccessibilityRole } from 'react-native';
import { connect } from 'react-redux';
import Tag from '@joplin/lib/models/Tag';
import { themeStyle } from '../global-style';
@@ -8,10 +8,15 @@ import { ScreenHeader } from '../ScreenHeader';
import { _ } from '@joplin/lib/locale';
import { AppState } from '../../utils/types';
import { TagEntity } from '@joplin/lib/services/database/types';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo, useState, useContext } from 'react';
import { Dispatch } from 'redux';
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
import { DialogContext } from '../DialogManager';
import useOnLongPressProps from '../../utils/hooks/useOnLongPressProps';
import { substrWithEllipsis } from '@joplin/lib/string-utils';
import { PromptButtonSpec } from '../DialogManager/types';
import MultiTouchableOpacity from '../buttons/MultiTouchableOpacity';
import SearchBar from './SearchScreen/SearchBar';
import Logger from '@joplin/utils/Logger';
@@ -47,12 +52,47 @@ const useStyles = (themeId: number) => {
}, [themeId]);
};
interface TagItemProps {
tag: TagEntity;
themeId: number;
onPress: (id: string)=> void;
onLongPress: (tag: TagEntity)=> void;
}
const TagItem: React.FC<TagItemProps> = ({ tag, themeId, onPress, onLongPress }) => {
const styles = useStyles(themeId);
const onLongPressProps = useOnLongPressProps({ onLongPress: () => onLongPress(tag), actionDescription: _('Edit tag') });
const accessibilityRole: AccessibilityRole = 'button';
const pressableProps = {
accessibilityRole,
accessibilityHint: _('Shows notes for tag'),
...onLongPressProps,
};
return (
<MultiTouchableOpacity
{...pressableProps}
containerProps={{
style: {},
}}
onPress={() => onPress(tag.id)}
beforePressable={null}
>
<View style={styles.listItem}>
<Text style={styles.listItemText}>{tag.title}</Text>
</View>
</MultiTouchableOpacity>
);
};
const TagsScreenComponent: React.FC<Props> = props => {
const [tags, setTags] = useState<TagEntity[]>([]);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
const [showSearch, setShowSearch] = useState(false);
const styles = useStyles(props.themeId);
const dialogs = useContext(DialogContext);
const collatorLocale = getCollatorLocale();
const collator = useMemo(() => {
return getCollator(collatorLocale);
@@ -86,7 +126,7 @@ const TagsScreenComponent: React.FC<Props> = props => {
setTags([]);
}
}
}, [searchQuery, collator], { interval: 200 });
}, [searchQuery, collator, refreshTrigger], { interval: 200 });
const onSearchButtonPress = useCallback(() => {
setShowSearch(!showSearch);
@@ -111,20 +151,74 @@ const TagsScreenComponent: React.FC<Props> = props => {
});
}, [props.dispatch]);
const onTagItemLongPress = useCallback(async (tag: TagEntity) => {
const menuItems: PromptButtonSpec[] = [];
const generateTagDeletion = () => {
return () => {
dialogs.prompt('', _('Delete tag "%s"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.', substrWithEllipsis(tag.title, 0, 32)), [
{
text: _('OK'),
onPress: async () => {
await Tag.delete(tag.id, { sourceDescription: 'tags-screen (long-press)' });
setRefreshTrigger(prev => prev + 1);
},
},
{
text: _('Cancel'),
onPress: () => { },
style: 'cancel',
},
]);
};
};
menuItems.push({
text: _('Rename'),
onPress: async () => {
const newName = await dialogs.promptForText(_('Rename tag:'), tag.title);
if (newName && newName.trim() && newName.trim() !== tag.title) {
try {
const updatedTag = { ...tag, title: newName };
await Tag.save(updatedTag, { fields: ['title'], userSideValidation: true });
setRefreshTrigger(prev => prev + 1);
} catch (error) {
await dialogs.error(error instanceof Error ? error.message : String(error));
}
}
},
});
menuItems.push({
text: _('Delete'),
onPress: generateTagDeletion(),
style: 'destructive',
});
menuItems.push({
text: _('Cancel'),
onPress: () => {},
style: 'cancel',
});
dialogs.prompt(
'',
_('Tag: %s', tag.title),
menuItems,
);
}, [dialogs]);
type RenderItemEvent = { item: TagEntity };
const onRenderItem = useCallback(({ item }: RenderItemEvent) => {
return (
<TouchableOpacity
onPress={() => onTagItemPress({ id: item.id })}
accessibilityRole='button'
accessibilityHint={_('Shows notes for tag')}
>
<View style={styles.listItem}>
<Text style={styles.listItemText}>{item.title}</Text>
</View>
</TouchableOpacity>
<TagItem
tag={item}
themeId={props.themeId}
onPress={(id) => onTagItemPress({ id })}
onLongPress={onTagItemLongPress}
/>
);
}, [onTagItemPress, styles]);
}, [onTagItemPress, onTagItemLongPress, props.themeId]);
return (
<View style={styles.rootStyle}>

View File

@@ -26,6 +26,7 @@ import { StateDecryptionWorker, StateResourceFetcher } from '@joplin/lib/reducer
import useOnLongPressProps from '../utils/hooks/useOnLongPressProps';
import { TouchableRipple } from 'react-native-paper';
import shim from '@joplin/lib/shim';
import getConflictFolderId from '@joplin/lib/models/utils/getConflictFolderId';
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
interface Props {
@@ -338,6 +339,8 @@ const SideMenuContentComponent = (props: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const menuItems: any[] = [];
if (folder && folder.id === getConflictFolderId()) return;
if (folder && folder.id === getTrashFolderId()) {
menuItems.push({
text: _('Empty trash'),

View File

@@ -1516,7 +1516,7 @@ PODS:
- React
- react-native-saf-x (3.5.1):
- React-Core
- react-native-safe-area-context (5.5.2):
- react-native-safe-area-context (5.6.1):
- React-Core
- react-native-sqlite-storage (6.0.1):
- React-Core
@@ -1890,7 +1890,7 @@ PODS:
- React
- RNSecureRandom (1.0.1):
- React
- RNShare (12.1.2):
- RNShare (12.2.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2349,7 +2349,7 @@ SPEC CHECKSUMS:
react-native-quick-crypto: b475b71e7fa4dbf3446be55e8ad4ef2c58ac4f7f
react-native-rsa-native: a7931cdda1f73a8576a46d7f431378c5550f0c38
react-native-saf-x: 404f0f9a29cc6bf21d88582e054c45a11b28c22b
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
react-native-webview: 0dceb35a9d050f5fa55f7fe2d8c4d1903651eb7d
@@ -2395,7 +2395,7 @@ SPEC CHECKSUMS:
RNLocalize: 3c4d0abd777a546fa77bdb6caef85a87fb9ea349
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 6496fc1ea6e8fce76b769513b6c2852f9c3ded82
RNShare: 40ace3f87cd881869e8085aced9dc16b425c74aa
RNSVG: 295a96bc43f2baa5958d64aeec9847a1d8ca7a3d
RNVectorIcons: e431ef1e6bef75d6ad0e33a83d376e6207962a9d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748

View File

@@ -28,6 +28,7 @@
"@joplin/react-native-saf-x": "~3.5",
"@joplin/renderer": "~3.5",
"@joplin/utils": "~3.5",
"@js-draw/material-icons": "1.32.0",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-community/geolocation": "3.4.0",
@@ -45,6 +46,7 @@
"expo-av": "15.1.7",
"expo-camera": "16.1.11",
"expo-local-authentication": "16.0.5",
"js-draw": "1.32.0",
"lodash": "4.17.21",
"md5": "2.3.0",
"path-browserify": "1.0.1",
@@ -66,9 +68,9 @@
"react-native-quick-actions": "0.3.13",
"react-native-quick-crypto": "0.7.17",
"react-native-rsa-native": "2.0.5",
"react-native-safe-area-context": "5.5.2",
"react-native-safe-area-context": "5.6.1",
"react-native-securerandom": "1.0.1",
"react-native-share": "12.1.2",
"react-native-share": "12.2.0",
"react-native-sqlite-storage": "6.0.1",
"react-native-svg": "15.13.0",
"react-native-url-polyfill": "2.0.0",
@@ -94,7 +96,6 @@
"@joplin/tools": "~3.5",
"@joplin/turndown": "~4.0.80",
"@joplin/turndown-plugin-gfm": "~1.0.62",
"@js-draw/material-icons": "1.30.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@react-native-community/cli": "16.0.3",
"@react-native-community/cli-platform-android": "16.0.3",
@@ -117,12 +118,11 @@
"babel-plugin-react-native-web": "0.21.1",
"esbuild": "0.25.9",
"fast-deep-equal": "3.1.3",
"fs-extra": "11.2.0",
"fs-extra": "11.3.1",
"gulp": "4.0.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jetifier": "2.0.0",
"js-draw": "1.30.1",
"jsdom": "26.1.0",
"nodemon": "3.1.10",
"punycode": "2.3.1",

View File

@@ -19,7 +19,7 @@
},
"dependencies": {
"@joplin/utils": "~3.5",
"fs-extra": "11.2.0",
"fs-extra": "11.3.1",
"yargs": "17.7.2"
}
}

View File

@@ -8,7 +8,7 @@ import {
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command, rectangularSelection,
dropCursor,
} from '@codemirror/view';
import { history, undoDepth, redoDepth, standardKeymap, insertTab } from '@codemirror/commands';
import { history, undoDepth, redoDepth, standardKeymap, insertTab, simplifySelection } from '@codemirror/commands';
import { keymap, KeyBinding } from '@codemirror/view';
import { searchKeymap } from '@codemirror/search';
@@ -40,6 +40,7 @@ import ctrlKeyStateClassExtension from './extensions/modifierKeyCssExtension';
import ctrlClickLinksExtension from './extensions/links/ctrlClickLinksExtension';
import { RenderedContentContext } from './extensions/rendering/types';
import ctrlClickCheckboxExtension from './extensions/ctrlClickCheckboxExtension';
import editorSettingsExtension, { setEditorSettingsEffect } from './extensions/editorSettingsExtension';
// Newer versions of CodeMirror by default use Chrome's EditContext API.
// While this might be stable enough for desktop use, it causes significant
@@ -235,6 +236,11 @@ const createEditor = (
}, true),
...standardKeymap, ...historyKeymap, ...searchKeymap,
// The escape -> simplifySelection mapping is present in "defaultKeymap",
// which is disabled on desktop but enabled on mobile. Enable this mapping
// globally for consistency:
keyCommand('Escape', simplifySelection, true),
]));
const editor = new EditorView({
@@ -295,6 +301,7 @@ const createEditor = (
biDirectionalTextExtension,
overwriteModeExtension,
ctrlKeyStateClassExtension,
editorSettingsExtension(settings),
selectedNoteIdExtension,
@@ -340,9 +347,12 @@ const createEditor = (
onSettingsChange: (newSettings: EditorSettings) => {
settings = newSettings;
editor.dispatch({
effects: dynamicConfig.reconfigure(
configFromSettings(newSettings, context),
),
effects: [
dynamicConfig.reconfigure(
configFromSettings(newSettings, context),
),
setEditorSettingsEffect.of(newSettings),
],
});
},
onUndoRedo: () => {

View File

@@ -1,5 +1,6 @@
import { EditorView } from '@codemirror/view';
import { Prec } from '@codemirror/state';
import { editorSettingsFacet } from './editorSettingsExtension';
const hasMultipleCursors = (view: EditorView) => {
return view.state.selection.ranges.length > 1;
@@ -12,7 +13,9 @@ const ctrlClickActionExtension = (onCtrlClick: OnCtrlClick) => {
Prec.high([
EditorView.domEventHandlers({
mousedown: (event: MouseEvent, view: EditorView) => {
const hasModifier = event.ctrlKey || event.metaKey;
const editorSettings = view.state.facet(editorSettingsFacet);
const hasModifier = editorSettings.preferMacShortcuts ? event.metaKey : event.ctrlKey;
// The default CodeMirror action for ctrl-click is to add another cursor
// to the document. If the user already has multiple cursors, assume that
// the ctrl-click action is intended to add another.

View File

@@ -0,0 +1,25 @@
import { Facet, StateEffect, StateField } from '@codemirror/state';
import { EditorSettings } from '../../types';
export const setEditorSettingsEffect = StateEffect.define<EditorSettings>();
export const editorSettingsFacet = Facet.define<EditorSettings|null, EditorSettings|null>({
combine: (possibleValues) => {
return possibleValues.filter(value => !!value)[0] ?? null;
},
});
export default (initialSettings: EditorSettings) => [
StateField.define<EditorSettings|null>({
create: () => initialSettings,
update: (oldValue, transaction) => {
for (const e of transaction.effects) {
if (e.is(setEditorSettingsEffect)) {
return e.value;
}
}
return oldValue;
},
provide: (field) => editorSettingsFacet.from(field),
}),
];

View File

@@ -1,6 +1,9 @@
import { StateEffect, StateField, Transaction } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { editorSettingsFacet } from './editorSettingsExtension';
// On MacOS: Tracks the meta key
// On other platforms: Tracks the ctrl key.
const ctrlOrMetaChangedEffect = StateEffect.define<boolean>();
const ctrlOrMetaPressedField = StateField.define<boolean>({
@@ -18,11 +21,13 @@ const ctrlOrMetaPressedField = StateField.define<boolean>({
})),
...(() => {
const onEvent = (event: KeyboardEvent|MouseEvent, view: EditorView) => {
const ctrlOrCmdPressed = event.ctrlKey || event.metaKey;
if (ctrlOrCmdPressed !== view.state.field(ctrlOrMetaPressedField)) {
const editorSettings = view.state.facet(editorSettingsFacet);
const hasModifier = editorSettings.preferMacShortcuts ? event.metaKey : event.ctrlKey;
if (hasModifier !== view.state.field(ctrlOrMetaPressedField)) {
view.dispatch({
effects: [
ctrlOrMetaChangedEffect.of(ctrlOrCmdPressed),
ctrlOrMetaChangedEffect.of(hasModifier),
],
});
}

View File

@@ -9,12 +9,12 @@ const allowImageUrlsToBeFetched = async () => {
await Promise.resolve();
};
const createEditor = async (initialMarkdown: string, hasImage: boolean) => {
const createEditor = async (initialMarkdown: string, expectedTags: string[] = ['Image']) => {
const resolveImageSrc = jest.fn((src, counter) => Promise.resolve(`${src}?r=${counter}`));
const editor = await createTestEditor(
initialMarkdown,
EditorSelection.cursor(0),
hasImage ? ['Image'] : [],
expectedTags,
[renderBlockImages({ resolveImageSrc })],
);
await allowImageUrlsToBeFetched();
@@ -40,7 +40,7 @@ describe('renderBlockImages', () => {
{ spaceBefore: ' ', spaceAfter: ' ', alt: 'test' },
{ spaceBefore: '', spaceAfter: '', alt: '!!!!' },
])('should render images below their Markdown source (case %#)', async ({ spaceBefore, spaceAfter, alt }) => {
const editor = await createEditor(`${spaceBefore}![${alt}](:/0123456789abcdef0123456789abcdef)${spaceAfter}`, true);
const editor = await createEditor(`${spaceBefore}![${alt}](:/0123456789abcdef0123456789abcdef)${spaceAfter}`);
const images = findImages(editor);
expect(images).toHaveLength(1);
@@ -51,13 +51,13 @@ describe('renderBlockImages', () => {
// For now, only Joplin resources are rendered. This simplifies the implementation and avoids
// potentially-unwanted web requests when opening a note with only the editor open.
test('should not render web images', async () => {
const editor = await createEditor('![test](https://example.com/test.png)\n\n', true);
const editor = await createEditor('![test](https://example.com/test.png)\n\n');
const images = findImages(editor);
expect(images).toHaveLength(0);
});
test('should allow reloading specific images', async () => {
const editor = await createEditor('![test](:/a123456789abcdef0123456789abcdef)\n![test 2](:/b123456789abcdef0123456789abcde2)', true);
const editor = await createEditor('![test](:/a123456789abcdef0123456789abcdef)\n![test 2](:/b123456789abcdef0123456789abcde2)');
// Should have the expected original image URLs
expect(getImageUrls(editor)).toMatchObject([
@@ -98,7 +98,7 @@ describe('renderBlockImages', () => {
const widthAttr = width ? ` width="${width}"` : '';
const editor = await createEditor(
`${spaceBefore}<img src=":/0123456789abcdef0123456789abcdef" alt="${alt}"${widthAttr} />${spaceAfter}`,
false,
['HTMLTag'],
);
const images = findImages(editor);
@@ -117,7 +117,7 @@ describe('renderBlockImages', () => {
test('should render non-self-closing HTML img tags', async () => {
const editor = await createEditor(
'<img src=":/0123456789abcdef0123456789abcdef" alt="test" width="300">',
false,
['HTMLBlock'],
);
const images = findImages(editor);
@@ -128,7 +128,7 @@ describe('renderBlockImages', () => {
test('should not render HTML img tags with web URLs', async () => {
const editor = await createEditor(
'<img src="https://example.com/test.png" alt="test" />',
false,
['HTMLTag'],
);
const images = findImages(editor);
@@ -138,7 +138,7 @@ describe('renderBlockImages', () => {
test('should render both markdown and HTML images in same document', async () => {
const editor = await createEditor(
'![markdown](:/a123456789abcdef0123456789abcdef)\n\n<img src=":/b123456789abcdef0123456789abcde2" alt="html" width="400" />',
true,
['Image', 'HTMLTag'],
);
const images = findImages(editor);
@@ -151,7 +151,7 @@ describe('renderBlockImages', () => {
const editor = await createEditor(
// eslint-disable-next-line quotes
"<img src=':/0123456789abcdef0123456789abcdef' alt='test' width='250' />",
false,
['HTMLTag'],
);
const images = findImages(editor);

View File

@@ -1,4 +1,4 @@
import { addColumnAfter, addRowAfter, deleteColumn, deleteRow, tableEditing } from 'prosemirror-tables';
import { addColumnAfter, addRowAfter, deleteColumn, deleteRow, deleteTable, tableEditing } from 'prosemirror-tables';
import createFloatingButtonPlugin, { ToolbarType } from './utils/createFloatingButtonPlugin';
import addColumnRightIcon from '../vendor/icons/addColumnRight';
import addRowBelowIcon from '../vendor/icons/addRowBelow';
@@ -11,6 +11,13 @@ const tableCommand = (command: Command): Command => (state, dispatch, view) => {
return command(state, dispatch, view) && focusEditor(state, dispatch, view);
};
// By default, commands like deleteRow or deleteColumn don't delete the last
// row/column in the table. This command removes the table when there are no more
// rows/columns to delete:
const runCommandOrDeleteTable = (command: Command): Command => (state, dispatch, view) => {
return command(state, dispatch, view) || deleteTable(state, dispatch);
};
const tablePlugin = [
tableEditing({ allowTableNodeSelection: true }),
createFloatingButtonPlugin('table', [
@@ -27,12 +34,12 @@ const tablePlugin = [
{
icon: removeRowIcon,
label: (_) => _('Delete row'),
command: () => tableCommand(deleteRow),
command: () => tableCommand(runCommandOrDeleteTable(deleteRow)),
},
{
icon: removeColumnIcon,
label: (_) => _('Delete column'),
command: () => tableCommand(deleteColumn),
command: () => tableCommand(runCommandOrDeleteTable(deleteColumn)),
},
], ToolbarType.FloatAboveBelow),
];

View File

@@ -17,6 +17,7 @@ const createEditorSettings = (themeId: number) => {
highlightActiveLine: false,
keymap: EditorKeymap.Default,
preferMacShortcuts: false,
language: EditorLanguageType.Markdown,
themeData,

View File

@@ -180,6 +180,7 @@ export interface EditorSettings {
language: EditorLanguageType;
keymap: EditorKeymap;
preferMacShortcuts: boolean;
tabMovesFocus: boolean;
markdownMarkEnabled: boolean;

View File

@@ -28,7 +28,7 @@
"@adobe/css-tools": "4.4.4",
"@joplin/fork-htmlparser2": "^4.1.60",
"datauri": "4.1.0",
"fs-extra": "11.2.0",
"fs-extra": "11.3.1",
"html-entities": "1.4.0"
},
"devDependencies": {

View File

@@ -1,18 +1,20 @@
import * as convertHtmlToMarkdown from './convertNoteToMarkdown';
import { AppState, createAppDefaultState } from '../app.reducer';
import Note from '@joplin/lib/models/Note';
import { defaultState, State } from '../reducer';
import Note from '../models/Note';
import { MarkupLanguage } from '@joplin/renderer';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import Folder from '@joplin/lib/models/Folder';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
import Folder from '../models/Folder';
import { NoteEntity } from '../services/database/types';
import shim from '../shim';
describe('convertNoteToMarkdown', () => {
let state: AppState = undefined;
let state: State = undefined;
beforeEach(async () => {
state = createAppDefaultState({});
state = defaultState;
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
shim.showToast = jest.fn();
});
it('should set the original note to be trashed', async () => {
@@ -29,13 +31,6 @@ describe('convertNoteToMarkdown', () => {
});
it('should recreate a new note that is a clone of the original', async () => {
let noteConvertedToMarkdownId = '';
const dispatchFn = jest.fn()
.mockImplementationOnce(() => {})
.mockImplementationOnce(action => {
noteConvertedToMarkdownId = action.id;
});
const folder = await Folder.save({ title: 'test_folder' });
const htmlNoteProperties = {
title: 'test',
@@ -49,10 +44,11 @@ describe('convertNoteToMarkdown', () => {
const htmlNote = await Note.save(htmlNoteProperties);
state.selectedNoteIds = [htmlNote.id];
await convertHtmlToMarkdown.runtime().execute({ state, dispatch: dispatchFn });
await convertHtmlToMarkdown.runtime().execute({ state, dispatch: jest.fn() });
expect(dispatchFn).toHaveBeenCalledTimes(2);
expect(noteConvertedToMarkdownId).not.toBe('');
const notes = await Note.previews(folder.id);
expect(notes).toHaveLength(1);
const noteConvertedToMarkdownId = notes[0].id;
const markdownNote = await Note.load(noteConvertedToMarkdownId);
@@ -63,15 +59,6 @@ describe('convertNoteToMarkdown', () => {
});
it('should generate action to trigger notification', async () => {
let originalHtmlNoteId = '';
let actionType = '';
const dispatchFn = jest.fn()
.mockImplementationOnce(action => {
originalHtmlNoteId = action.value;
actionType = action.type;
})
.mockImplementationOnce(() => {});
const folder = await Folder.save({ title: 'test_folder' });
const htmlNoteProperties = {
title: 'test',
@@ -85,12 +72,9 @@ describe('convertNoteToMarkdown', () => {
const htmlNote = await Note.save(htmlNoteProperties);
state.selectedNoteIds = [htmlNote.id];
await convertHtmlToMarkdown.runtime().execute({ state, dispatch: dispatchFn });
await convertHtmlToMarkdown.runtime().execute({ state, dispatch: jest.fn() });
expect(dispatchFn).toHaveBeenCalledTimes(2);
expect(originalHtmlNoteId).toBe(htmlNote.id);
expect(actionType).toBe('NOTE_HTML_TO_MARKDOWN_DONE');
expect(shim.showToast).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,75 @@
import { _, _n } from '../locale';
import Note from '../models/Note';
import { CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
import { MarkupLanguage } from '@joplin/renderer';
import { runtime as convertHtmlToMarkdown } from './convertHtmlToMarkdown';
import shim, { ToastType } from '../shim';
import { NoteEntity } from '../services/database/types';
import { itemIsReadOnly } from '../models/utils/readOnly';
import { ModelType } from '../BaseModel';
import ItemChange from '../models/ItemChange';
import Setting from '../models/Setting';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('convertNoteToMarkdown');
export const declaration: CommandDeclaration = {
name: 'convertNoteToMarkdown',
label: () => _('Convert to Markdown'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, noteIds: string|string[] = []) => {
if (typeof noteIds === 'string') {
noteIds = [noteIds];
}
if (noteIds.length === 0) {
noteIds = context.state.selectedNoteIds;
}
const notes: NoteEntity[] = await Note.loadItemsByIdsOrFail(noteIds);
try {
let isFirst = true;
let processedCount = 0;
for (const note of notes) {
if (note.markup_language === MarkupLanguage.Markdown) {
logger.warn('Skipping item: Already Markdown.');
continue;
}
if (await itemIsReadOnly(Note, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note.id, Setting.value('sync.userId'), context.state.shareService)) {
throw new Error(_('Cannot convert read-only item: "%s"', note.title));
}
const markdownBody = await convertHtmlToMarkdown().execute(context, note.body);
const newNote = await Note.duplicate(note.id);
newNote.body = markdownBody;
newNote.markup_language = MarkupLanguage.Markdown;
await Note.save(newNote);
await Note.delete(note.id, { toTrash: true });
processedCount ++;
if (isFirst) {
context.dispatch({
type: 'NOTE_SELECT',
id: newNote.id,
});
isFirst = false;
}
}
void shim.showToast(_n(
'The note has been converted to Markdown and the original note has been moved to the trash',
'The notes have been converted to Markdown and the original notes have been moved to the trash',
processedCount,
), { type: ToastType.Success });
} catch (error) {
await shim.showErrorDialog(_('Could not convert notes to Markdown: %s', error.message));
}
},
enabledCondition: 'selectionIncludesHtmlNotes && (multipleNotesSelected || !noteIsReadOnly)',
};
};

View File

@@ -1,5 +1,6 @@
// AUTO-GENERATED using `gulp buildScriptIndexes`
import * as convertHtmlToMarkdown from './convertHtmlToMarkdown';
import * as convertNoteToMarkdown from './convertNoteToMarkdown';
import * as deleteNote from './deleteNote';
import * as historyBackward from './historyBackward';
import * as historyForward from './historyForward';
@@ -14,6 +15,7 @@ import * as toggleEditorPlugin from './toggleEditorPlugin';
const index: any[] = [
convertHtmlToMarkdown,
convertNoteToMarkdown,
deleteNote,
historyBackward,
historyForward,

View File

@@ -66,7 +66,7 @@
"file-type": "16.5.4",
"follow-redirects": "1.15.11",
"form-data": "4.0.4",
"fs-extra": "11.2.0",
"fs-extra": "11.3.1",
"hpagent": "1.2.0",
"html-entities": "1.4.0",
"html-minifier": "4.0.0",

View File

@@ -271,7 +271,10 @@ export default class RevisionService extends BaseService {
}
public async restoreFolder() {
let folder = await Folder.loadByTitle(this.restoreFolderTitle());
let folder = await Folder.loadByFields({
title: this.restoreFolderTitle(),
deleted_time: 0,
});
if (!folder) {
folder = await Folder.save({ title: this.restoreFolderTitle() });
}

View File

@@ -8,6 +8,7 @@ import { itemIsReadOnlySync, ItemSlice } from '../../models/utils/readOnly';
import ItemChange from '../../models/ItemChange';
import { getTrashFolderId } from '../trash';
import getActivePluginEditorView from '../plugins/utils/getActivePluginEditorView';
import { MarkupLanguage } from '@joplin/renderer';
export interface WhenClauseContextOptions {
commandFolderId?: string;
@@ -18,6 +19,7 @@ export interface WhenClauseContextOptions {
export interface WhenClauseContext {
allSelectedNotesAreDeleted: boolean;
selectionIncludesHtmlNotes: boolean;
foldersAreDeleted: boolean;
foldersIncludeReadOnly: boolean;
folderIsDeleted: boolean;
@@ -88,6 +90,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
// Selected notes properties
allSelectedNotesAreDeleted: !selectedNotes.find(n => !n.deleted_time),
selectionIncludesHtmlNotes: selectedNotes.some(n => n.markup_language === MarkupLanguage.Html),
// Note history
historyhasBackwardNotes: windowState.backwardHistoryNotes && windowState.backwardHistoryNotes.length > 0,

View File

@@ -280,4 +280,11 @@ describe('InteropService_Importer_Md', () => {
expect(resources[2].title).toBe('file3.text');
expect(resources[2].file_extension).toBe('text');
});
it('should not fail when importing a file with a long URL', async () => {
await importNote(`${supportDir}/test_notes/md/long-url.md`);
const note: NoteEntity = (await Note.all())[0];
expect(note.title).toBe('long-url');
expect(note.body).toBe('# test for joplin import\n\n[https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm\\_source%3DYouTube%2520Instagram%26utm\\_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm\\_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm\\_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm\\_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm\\_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o\\_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&\\_\\_tn\\_\\_=H-y-R&c[0]=AT0eE6OXx\\_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887\\_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG\\_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo76663333hhsgsu](<https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm_source%3DYouTube%2520Instagram%26utm_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&__tn__=H-y-R&c[0]=AT0eE6OXx_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo>)');
});
});

View File

@@ -127,6 +127,11 @@ export default class InteropService_Importer_Md extends InteropService_Importer_
const linkPosix = toForwardSlashes(link);
const trimmedLink = this.trimAnchorLink(linkPosix);
const pathWithExtension = shim.fsDriver().resolve(`${dirname(filePath)}/${trimmedLink}`);
// This check also means that non-files, such as web URLs, will not be processed by
// the code below and simply inserted as links.
if (!(await shim.fsDriver().exists(pathWithExtension))) continue;
const stat = await shim.fsDriver().stat(pathWithExtension);
const isDir = stat ? stat.isDirectory() : false;
if (stat && !isDir) {

View File

@@ -6,6 +6,18 @@ import { ImportModuleOutputFormat, ImportOptions } from './types';
import InteropService from './InteropService';
import Folder from '../../models/Folder';
import { NoteEntity } from '../database/types';
const moment = require('moment');
// Suppress warning:
//
// Deprecation warning: value provided is not in a recognized RFC2822 or ISO format. moment
// construction falls back to js Date(), which is not reliable across all browsers and versions. Non
// RFC2822/ISO date formats are discouraged. Please refer to
// http://momentjs.com/guides/#/warnings/js-date/ for more info.
//
// But what moment.js does it correct when you don't know the format of the date, which is what we
// simulate here with imported files.
moment.suppressDeprecationWarnings = true;
async function importNote(path: string) {
const folder = await Folder.save({});
@@ -26,7 +38,7 @@ const importTestFile = async (name: string): Promise<NoteEntity> => {
return importNote(`${supportDir}/test_notes/yaml/${name}`);
};
describe('InteropService_Importer_Md_frontmatter: importMetadata', () => {
describe('InteropService_Importer_Md_frontmatter', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
@@ -144,8 +156,12 @@ describe('InteropService_Importer_Md_frontmatter: importMetadata', () => {
const note = await importTestFile('notesnook_updated_created.md');
expect(note.title).toBe('Frontmatter test');
// Notesnook seems to export data with an ambiguous date format such as "02-01-2024" (Is
// that 1 February or 2 January?) and we can't support something like this, so there's a
// chance imported Notesnook will have the wrong dates but it's for them to fix their app.
expect(note.user_created_time).toBe(Date.parse('2024-01-01T01:23:00.000'));
expect(note.user_updated_time).toBe(Date.parse('2024-01-02T04:56:00.000'));
expect(note.user_updated_time).toBe(Date.parse('2024-01-01T04:56:00.000'));
});
it('should handle date formats with timezone information', async () => {
const note = await importTestFile('utc.md');
@@ -205,6 +221,10 @@ describe('InteropService_Importer_Md_frontmatter: importMetadata', () => {
});
});
it('should not fail if the keywords field is empty', async () => {
await expect(importTestFile('bad_keywords.md')).resolves.not.toThrow();
});
it('should use the filename in cases where the frontmatter has no title', async () => {
const note = await importTestFile('filename-title.md');

View File

@@ -226,7 +226,7 @@ describe('InteropService_Importer_OneNote', () => {
const noteToTest = notes.find(n => n.title === 'Tips from a Pro Using Trees for Dramatic Landscape Photography');
expectWithInstructions(noteToTest).toBeTruthy();
expectWithInstructions(noteToTest.body.includes('<a href="onenote:https://d.docs.live.net/c8d3bbab7f1acf3a/Documents/Photography/风景.one#Tips%20from%20a%20Pro%20Using%20Trees%20for%20Dramatic%20Landscape%20Photography&section-id={262ADDFB-A4DC-4453-A239-0024D6769962}&page-id={88D803A5-4F43-48D4-9B16-4C024F5787DC}&end" style="">Tips from a Pro: Using Trees for Dramatic Landscape Photography</a>')).toBe(true);
expectWithInstructions(noteToTest.body).toContain('<a href="onenote:https://d.docs.live.net/c8d3bbab7f1acf3a/Documents/Photography/%E9%A3%8E%E6%99%AF.one#Tips%20from%20a%20Pro%20Using%20Trees%20for%20Dramatic%20Landscape%20Photography&section-id={262ADDFB-A4DC-4453-A239-0024D6769962}&page-id={88D803A5-4F43-48D4-9B16-4C024F5787DC}&end" style="">Tips from a Pro: Using Trees for Dramatic Landscape Photography</a>');
});
it('should render links properly by ignoring wrongly set indices when the first character is a hyperlink marker', async () => {
@@ -307,4 +307,14 @@ describe('InteropService_Importer_OneNote', () => {
expect(markdown).toMatchSnapshot('Test Todo: As Markdown');
});
it('should correctly import math formulas', async () => {
const notes = await importNote(`${supportDir}/onenote/Math.one`);
const importedNote = notes.find(n => n.title.startsWith('Math'));
const converter = new HtmlToMd();
const markdown = converter.parse(importedNote.body);
expect(markdown).toMatchSnapshot('Math');
});
});

View File

@@ -142,7 +142,7 @@ jeudi 23 octobre 2025
- [x] Rédiger carnet MS-OneNote jeu d'essai (case cochée)
- [ ] Transmettre aux dev Joplin (case non cochée)
- [x] Exporter le carnet en \\*.packageone (case cochée)
- [x] Exporter le carnet en \\*.onepkg (case cochée)
- [ ] Exporter des page en \\*.one (case non cochée)
&nbsp;
@@ -155,6 +155,44 @@ jeudi 23 octobre 2025
- [x] Documenter configuration synchro JBS saml pour un utilisateur (case cochée)"
`;
exports[`InteropService_Importer_OneNote should correctly import math formulas: Math 1`] = `
" Math
Math
Friday, November 28, 2025
2:47 PM
Cauchy's Integral Formula: $\\def\\∫#1#2#3{\\int_{#1}^{#2}{#3}}\\def\\parens#1{\\left( {#1} \\right)}𝑓\\parens{𝑥}=\\∫{𝛾}{}{\\frac{𝑓(𝑧)}{𝑧−𝑥}𝑑𝑧}$
Pythagorean Theorem: $\\def\\pow#1#2{{#1}^{#2}}\\pow{𝑎}{2}+\\pow{𝑏}{2}=\\pow{𝑐}{2}$
Law of Cosines: $\\def\\pow#1#2{{#1}^{#2}}\\def\\fnCall#1#2{{ \\rm #1 }\\ {#2}}\\pow{𝑎}{2}+\\pow{𝑏}{2}=\\pow{𝑐}{2}+2𝑎𝑏\\fnCall{cos}{𝐶}$
Euler's Formula: $\\def\\pow#1#2{{#1}^{#2}}\\def\\fnCall#1#2{{ \\rm #1 }\\ {#2}}\\pow{𝑒}{𝑖𝜃}=\\fnCall{cos}{𝜃}+𝑖\\fnCall{sin}{𝜃}$
Determinant of a 2x2 matrix: $\\def\\parens#1{\\left( {#1} \\right)}\\def\\unknown#1{\\textsf{Unknown}(#1)}\\parens{\\unknown{20}{𝑎}{𝑏}{𝑐}{𝑑}}=𝑎𝑑−𝑏𝑐$
Empty formula: $\\mathrm{Type equation here.}$
Fractions: $\\frac{𝑎}{𝑏}, \\frac{𝑎}{𝑏},\\mathrm{\\frac{𝑎}{𝑏}}, \\frac{𝑎}{𝑏}$.
Summation: $\\def\\∑#1#2#3{\\sum_{#1}^{#2}{#3}}\\def\\withSubscript#1#2{{#1}_{#2}}\\def\\fnCall#1#2{{ \\rm #1 }\\ {#2}}\\fnCall{\\withSubscript{lim}{𝑛→∞}}{\\∑{𝑘=0}{𝑛 }{\\frac{1}{𝑘}}}=∞$.
Definite integral: $\\def\\∫#1#2#3{\\int_{#1}^{#2}{#3}}\\∫{0}{1}{𝑥}ⅆ𝑥=\\frac{1}{2}$
&nbsp;
Integral identities (see https://en.wikipedia.org/wiki/Lists_of_integrals), below, $𝐶∈ℝ$:
- $\\def\\parens#1{\\left( {#1} \\right)}\\def\\fnCall#1#2{{ \\rm #1 }\\ {#2}}∫\\mathrm{\\frac{1}{𝑥}𝑑𝑥}=\\fnCall{ln}{\\parens{𝑥}}+𝐶$.
- $\\def\\pow#1#2{{#1}^{#2}}∫\\pow{𝑒}{𝑎𝑥}𝑑𝑥=\\frac{1}{𝑎}\\pow{𝑒}{𝑎𝑥}+𝐶$
- $\\def\\parens#1{\\left( {#1} \\right)}\\def\\fnCall#1#2{{ \\rm #1 }\\ {#2}}∫\\parens{\\fnCall{sin}{𝑥}}𝑑𝑥=−\\fnCall{cos}{𝑥}+𝐶$
&nbsp;"
`;
exports[`InteropService_Importer_OneNote should expect notes to be rendered the same: A page can have any width it wants 1`] = `
"<!DOCTYPE HTML>
<html lang="en"><head>
@@ -304,7 +342,7 @@ exports[`InteropService_Importer_OneNote should expect notes to be rendered the
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 115px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 10.5pt; padding-bottom: 7px; padding-top: 7px;">Suspendisse vitae odio nibh. Etiam fringilla mattis dapibus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce vel ultricies ligula. Sed a nunc ante. Praesent suscipit fermentum magna. Aliquam convallis porttitor lacus ac posuere. Vestibulum maximus leo vel tortor condimentum, et tristique leo maximus. Nulla elementum, augue eu sollicitudin tempus, arcu ex lacinia enim, ut posuere lectus libero non eros. Vestibulum a libero leo. Donec id leo commodo, ornare ante ac, molestie tellus. Aenean a neque quis turpis euismod porta. Quisque vulputate augue vitae orci accumsan, a lobortis leo luctus. Nunc sodales sapien vitae lacus faucibus hendrerit. In ac lacinia diam.</p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 7px; padding-top: 7px;"><span style="font-family: Calibri; font-size: 14pt;">Nam tempor urna eget posuere mollis. Aliquam erat volutpat. Sed ipsum massa, dictum eget sagittis id, fermentum a justo. Vivamus in iaculis libero. Pellentesque malesuada felis dictum turpis placerat, at ultrices justo viverra. Praesent nisi lectus, tincidunt ut tellus in, convallis euismod urna. Phasellus molestie porttitor odio vitae efficitur. Curabitur vulputate congue tincidunt. Fusce mattis orci at porttitor fermentum. Cras eu placerat odio. Fusce eu tortor sit amet massa pretium efficitur. Nam consequat, mauris at blandit placerat, est sapien feugiat felis, quis imperdiet sapien neque in justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus vestibulum rhoncus dolor, ut ullamcorper purus scelerisque eu. Integer sem felis, pellentesque in rutrum id, porta a ante.</span><span style="font-family: Calibri; font-size: 18pt;">Vivamus finibus imperdiet massa, at interdum turpis rhoncus et. Phasellus leo nibh, mattis vel tortor at, gravida finibus felis. Donec bibendum enim euismod, dignissim ipsum eu, laoreet nisl. Ut auctor sollicitudin eros dictum gravida.</span><span style="font-family: Calibri; font-size: 14pt;"> Vestibulum pellentesque, ex quis vulputate efficitur, dolor metus efficitur nisl, id elementum mi nulla sit amet orci. Nam odio sem, bibendum at hendrerit finibus, vestibulum vitae dolor. In hac habitasse platea dictumst. Curabitur et ligula elit. Donec vulputate, diam non gravida efficitur, mi odio imperdiet ipsum, nec rhoncus mi nibh non magna.</span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 10.5pt; padding-bottom: 7px; padding-top: 7px;">Suspendisse varius enim vel odio congue sodales. Integer sit amet nisi sagittis, dapibus mi ut, tincidunt magna. Duis posuere est felis, et rhoncus magna volutpat a. Nullam tempor dignissim suscipit. Vestibulum cursus felis vitae libero pulvinar molestie. Donec at metus eget arcu blandit tincidunt. Donec purus felis, malesuada ac egestas eu, interdum sed erat. Praesent nec accumsan orci. Nunc bibendum rutrum erat, vel luctus odio. Pellentesque iaculis gravida arcu, eu consequat turpis congue sit amet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Duis eget urna vel erat aliquet fringilla. Praesent vel luctus ligula, nec viverra nisl. Sed ac sem consectetur, sodales ante sodales, feugiat arcu.</p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt;"><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;">It was a bright cold day in April, and the clocks were striking thirteen. Winston Smith, his chin nuzzled into his breast in an effort to escape the vile wind, slipped quickly through the glass doors of Victory Mansions, though not quickly enough to prevent a swirl of gritty dust from entering along with him. </span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;">The hallway smelt of boiled cabbage and old rag mats. At one end of it a coloured poster, too large for indoor display, had been tacked to the wall. It depicted simply an enormous face, more than a metre wide: the face of a man of about forty-five, with a heavy black moustache and ruggedly handsome features. Winston made for the stairs. It was no use trying the lift. Even at the best of times it was seldom working, and at present the electric current was cut off during daylight hours. It was part of the economy drive in preparation for Hate Week. The flat was seven flights up, and Winston, who was thirty-nine and had a varicose ulcer above his right ankle, went slowly, resting several times on the way. On each landing, opposite the lift-shaft, the poster with the enormous face gazed from the wall. It was one of those pictures which are so contrived that the eyes follow you about when you move. BIG BROTHER IS WATCHING YOU, the caption beneath it ran. </span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;">Inside the flat a fruity voice was reading out a list of figures which had something to do with the production of pig-iron. The voice came from an oblong metal plaque like a dulled mirror which formed part of the surface of the right-hand wall. Winston turned a switch and the voice sank somewhat, though the words were still distinguishable. The instrument (the telescreen, it was called) could be dimmed, but there was no way of shutting it off completely. He moved over to the window: a smallish, frail figure, the meagreness of his body merely emphasized by the blue overalls which were the uniform of the party. His hair was very fair, his face naturally sanguine, his skin roughened by coarse soap and blunt razor blades and the cold of the winter that had just ended. </span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt;"><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;">It was a bright cold day in April, and the clocks were striking thirteen. Winston Smith, his chin nuzzled into his breast in an effort to escape the vile wind, slipped quickly through the glass doors of Victory Mansions, though not quickly enough to prevent a swirl of gritty dust from entering along with him. </span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;">The hallway smelt of boiled cabbage and old rag mats. At one end of it a coloured poster, too large for indoor display, had been tacked to the wall. It depicted simply an enormous face, more than a metre wide: the face of a man of about forty-five, with a heavy black moustache and ruggedly handsome features. Winston made for the stairs. It was no use trying the lift. Even at the best of times it was seldom working, and at present the electric current was cut off during daylight hours. It was part of the economy drive in preparation for Hate Week. The flat was seven flights up, and Winston, who was thirty-nine and had a varicose ulcer above his right ankle, went slowly, resting several times on the way. On each landing, opposite the lift-shaft, the poster with the enormous face gazed from the wall. It was one of those pictures which are so contrived that the eyes follow you about when you move. BIG BROTHER IS WATCHING YOU, the caption beneath it ran. </span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;">Inside the flat a fruity voice was reading out a list of figures which had something to do with the production of pig-iron. The voice came from an oblong metal plaque like a dulled mirror which formed part of the surface of the right-hand wall. Winston turned a switch and the voice sank somewhat, though the words were still distinguishable. The instrument (the telescreen, it was called) could be dimmed, but there was no way of shutting it off completely. He moved over to the window: a smallish, frail figure, the meagreness of his body merely emphasized by the blue overalls which were the uniform of the party. His hair was very fair, his face naturally sanguine, his skin roughened by coarse soap and blunt razor blades and the cold of the winter that had just ended. </span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span><span style="color: rgb(66,66,67); font-family: Calibri; font-size: 10pt;"><br></span>Outside, even through the shut window-pane, the world looked cold. Down in the street little eddies of wind were whirling dust and torn paper into spirals, and though the sun was shining and the sky a harsh blue, there seemed to be no colour in anything, except the posters that were plastered everywhere. The blackmoustachio'd face gazed down from every commanding corner. There was one on the house-front immediately opposite. BIG BROTHER IS WATCHING YOU, the caption said, while the dark eyes looked deep into Winston's own. Down at streetlevel another poster, torn at one corner, flapped fitfully in the wind, alternately covering and uncovering the single word INGSOC. In the far distance a helicopter skimmed down between the roofs, hovered for an instant like a bluebottle, and darted away again with a curving flight. It was the police patrol, snooping into people's windows. The patrols did not matter, however. Only the Thought Police mattered. <br><br>Behind Winston's back the voice from the telescreen was still babbling away about pig-iron and the overfulfilment of the Ninth Three-Year Plan. The telescreen received and transmitted simultaneously. Any sound that Winston made, above the level of a very low whisper, would be picked up by it, moreover, so long as he remained within the field of vision which the metal plaque commanded, he could be seen as well as heard. There was of course no way of knowing whether you were being watched at any given moment. How often, or on what system, the Thought Police plugged in on any individual wire was guesswork. It was even conceivable that they watched everybody all the time. But at any rate they could plug in your wire whenever they wanted to. You had to live -- did live, from habit that became instinct -- in the assumption that every sound you made was overheard, and, except in darkness, every movement scrutinized. </p></div>
</div><img style="height: 6px; left: 49px; overflow: visible; position: absolute; top: 127px; width: 72px;" src=":/204"><img style="height: 16px; left: 465px; overflow: visible; position: absolute; top: 227px; width: 17px;" src=":/205"><img style="height: 7px; left: 49px; overflow: visible; position: absolute; top: 262px; width: 37px;" src=":/206"><img style="height: 9px; left: 146px; overflow: visible; position: absolute; top: 521px; width: 82px;" src=":/207"><img style="height: 17px; left: 175px; overflow: visible; position: absolute; top: 713px; width: 28px;" src=":/208"><img style="height: 6px; left: 46px; overflow: visible; position: absolute; top: 883px; width: 12px;" src=":/209"><img style="height: 12px; left: 157px; overflow: visible; position: absolute; top: 1518px; width: 17px;" src=":/210">
<script>
@@ -788,17 +826,17 @@ exports[`InteropService_Importer_OneNote should ignore broken characters at the
<div class="title" style="left: 48px; position: absolute; top: 24px;"><div class="container-outline" style="width: 624px;"><div class="outline-element" style="margin-left: 0px;"><span style="font-family: Calibri Light; font-size: 20pt;">Action research - Wikipedia</span></div>
</div><div class="container-outline"><div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(128,128,128); font-family: Calibri; font-size: 10pt;">Monday, May 27, 2019</span></div>
<div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(128,128,128); font-family: Calibri; font-size: 10pt;">12:13 PM</span></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 120px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 7px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">Clipped from: </span><a href="https://en.wikipedia.org/wiki/Action_research#Action_research_in_organization_development" style="">https://en.wikipedia.org/wiki/Action_research#Action_research_in_organization_development</a></p></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 120px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 7px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">Clipped from: </span><a href="https://en.wikipedia.org/wiki/Action_research#Action_research_in_organization_development" style="font-family: Verdana; font-size: 12pt;">https://en.wikipedia.org/wiki/Action_research#Action_research_in_organization_development</a></p></div>
<div class="outline-element" style="margin-left: 0px;"><img src=":/5" style="max-height: -48px; max-width: -48px;" /></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">569 revisions since </span><a href="https://en.wikipedia.org/wiki/Special%3APermaLink%2F2264241" style="">2003-05-19</a><span style="font-family: Verdana; font-size: 12pt;"> (</span><a href="https://en.wikipedia.org/wiki/Special%3ADiff%2F898227450" style="">+5 days</a><span style="font-family: Verdana; font-size: 12pt;">), 328 editors, 90 watchers, </span><a href="https://tools.wmflabs.org/pageviews?project=en.wikipedia.org&pages=Action%20research&range=latest-30" style="">18,937 pageviews</a><span style="font-family: Verdana; font-size: 12pt;"> (30 days), created by: </span><a href="https://en.wikipedia.org/wiki/User:Thseamon" style="">Thseamon</a><span style="font-family: Verdana; font-size: 12pt;"> (</span><a href="http://xtools.wmflabs.org/ec/en.wikipedia.org/Thseamon" style="">762</a><span style="font-family: Verdana; font-size: 12pt;">) · </span><a href="http://xtools.wmflabs.org/articleinfo/en.wikipedia.org/Action%20research" style="">See full page statistics</a><span style="font-family: Verdana; font-size: 12pt;"> </span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><a href="https://en.wikipedia.org/wiki/Action_research#mw-head" style="">Jump to navigation</a><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://en.wikipedia.org/wiki/Action_research#p-search" style="">Jump to search</a><span style="font-family: Verdana; font-size: 12pt;"> </span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">For the British charity formerly named Action Research, see </span><a href="https://en.wikipedia.org/wiki/Action_Medical_Research" style="">Action Medical Research</a><span style="font-family: Verdana; font-size: 12pt;">. For the academic journal titled Action Research, see </span><a href="https://en.wikipedia.org/wiki/Action_Research_(journal)" style="">Action Research (journal)</a><span style="font-family: Verdana; font-size: 12pt;">.</span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">569 revisions since </span><a href="https://en.wikipedia.org/wiki/Special%3APermaLink%2F2264241" style="font-family: Verdana; font-size: 12pt;">2003-05-19</a><span style="font-family: Verdana; font-size: 12pt;"> (</span><a href="https://en.wikipedia.org/wiki/Special%3ADiff%2F898227450" style="font-family: Verdana; font-size: 12pt;">+5 days</a><span style="font-family: Verdana; font-size: 12pt;">), 328 editors, 90 watchers, </span><a href="https://tools.wmflabs.org/pageviews?project=en.wikipedia.org&pages=Action%20research&range=latest-30" style="font-family: Verdana; font-size: 12pt;">18,937 pageviews</a><span style="font-family: Verdana; font-size: 12pt;"> (30 days), created by: </span><a href="https://en.wikipedia.org/wiki/User:Thseamon" style="font-family: Verdana; font-size: 12pt;">Thseamon</a><span style="font-family: Verdana; font-size: 12pt;"> (</span><a href="http://xtools.wmflabs.org/ec/en.wikipedia.org/Thseamon" style="font-family: Verdana; font-size: 12pt;">762</a><span style="font-family: Verdana; font-size: 12pt;">) · </span><a href="http://xtools.wmflabs.org/articleinfo/en.wikipedia.org/Action%20research" style="font-family: Verdana; font-size: 12pt;">See full page statistics</a><span style="font-family: Verdana; font-size: 12pt;"> </span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><a href="https://en.wikipedia.org/wiki/Action_research#mw-head" style="font-family: Verdana; font-size: 12pt;">Jump to navigation</a><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://en.wikipedia.org/wiki/Action_research#p-search" style="font-family: Verdana; font-size: 12pt;">Jump to search</a><span style="font-family: Verdana; font-size: 12pt;"> </span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">For the British charity formerly named Action Research, see </span><a href="https://en.wikipedia.org/wiki/Action_Medical_Research" style="font-family: Verdana; font-size: 12pt;">Action Medical Research</a><span style="font-family: Verdana; font-size: 12pt;">. For the academic journal titled Action Research, see </span><a href="https://en.wikipedia.org/wiki/Action_Research_(journal)" style="font-family: Verdana; font-size: 12pt;">Action Research (journal)</a><span style="font-family: Verdana; font-size: 12pt;">.</span></p></div>
<div class="outline-element" style="margin-left: 0px;"><table cellpadding="0" cellspacing="0" style="border-collapse: collapse;"><tr><td style="min-width: 48px; padding: 2pt; vertical-align: top;"><div class="outline-element" style="margin-left: 0px;"><img src=":/6" style="max-height: 39px; max-width: 50px;" /></div>
</td><td style="min-width: 48px; padding: 2pt; vertical-align: top;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt;"><span style="font-family: Verdana; font-size: 12pt;">This article </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">needs additional citations for </span><a href="https://en.wikipedia.org/wiki/Wikipedia:Verifiability" style="">verification</a><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">. Please help </span><a href="https://en.wikipedia.org/w/index.php?title=Action_research&action=edit" style="">improve this article</a><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;"> by </span><a href="https://en.wikipedia.org/wiki/Help:Introduction_to_referencing_with_Wiki_Markup/1" style="">adding citations to reliable sources</a><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">. Unsourced material may be challenged and removed.</span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;"><br></span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">Find sources:</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://www.google.com/search?as_eq=wikipedia&q=%22Action+research%22" style="">"Action research"</a><span style="font-family: Verdana; font-size: 12pt;"> – </span><a href="https://www.google.com/search?tbm=nws&q=%22Action+research%22+-wikipedia" style="">news</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">·</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://www.google.com/search?&q=%22Action+research%22+site:news.google.com/newspapers&source=newspapers" style="">newspapers</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">·</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://www.google.com/search?tbs=bks:1&q=%22Action+research%22+-wikipedia" style="">books</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">·</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://scholar.google.com/scholar?q=%22Action+research%22" style="">scholar</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">·</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://www.jstor.org/action/doBasicSearch?Query=%22Action+research%22&acc=on&wc=on" style="">JSTOR</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-style: italic;">(May 2019)</span><span style="font-family: Verdana; font-size: 12pt; font-style: italic;"> (</span><a href="https://en.wikipedia.org/wiki/Help:Maintenance_template_removal" style="">Learn how and when to remove this template message</a><span style="font-family: Verdana; font-size: 12pt; font-style: italic;">)</span></p></div>
</td><td style="min-width: 48px; padding: 2pt; vertical-align: top;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt;"><span style="font-family: Verdana; font-size: 12pt;">This article </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">needs additional citations for </span><a href="https://en.wikipedia.org/wiki/Wikipedia:Verifiability" style="font-family: Verdana; font-size: 12pt; font-weight: bold;">verification</a><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">. Please help </span><a href="https://en.wikipedia.org/w/index.php?title=Action_research&action=edit" style="font-family: Verdana; font-size: 12pt; font-weight: bold;">improve this article</a><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;"> by </span><a href="https://en.wikipedia.org/wiki/Help:Introduction_to_referencing_with_Wiki_Markup/1" style="font-family: Verdana; font-size: 12pt; font-weight: bold;">adding citations to reliable sources</a><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">. Unsourced material may be challenged and removed.</span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;"><br></span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">Find sources:</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://www.google.com/search?as_eq=wikipedia&q=%22Action+research%22" style="font-family: Verdana; font-size: 12pt;">&quot;Action research&quot;</a><span style="font-family: Verdana; font-size: 12pt;"> – </span><a href="https://www.google.com/search?tbm=nws&q=%22Action+research%22+-wikipedia" style="font-family: Verdana; font-size: 12pt;">news</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">·</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://www.google.com/search?&q=%22Action+research%22+site:news.google.com/newspapers&source=newspapers" style="font-family: Verdana; font-size: 12pt;">newspapers</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">·</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://www.google.com/search?tbs=bks:1&q=%22Action+research%22+-wikipedia" style="font-family: Verdana; font-size: 12pt;">books</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">·</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://scholar.google.com/scholar?q=%22Action+research%22" style="font-family: Verdana; font-size: 12pt;">scholar</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">·</span><span style="font-family: Verdana; font-size: 12pt;"> </span><a href="https://www.jstor.org/action/doBasicSearch?Query=%22Action+research%22&acc=on&wc=on" style="font-family: Verdana; font-size: 12pt;">JSTOR</a><span style="font-family: Verdana; font-size: 12pt;"> </span><span style="font-family: Verdana; font-size: 12pt; font-style: italic;">(May 2019)</span><span style="font-family: Verdana; font-size: 12pt; font-style: italic;"> (</span><a href="https://en.wikipedia.org/wiki/Help:Maintenance_template_removal" style="font-family: Verdana; font-size: 12pt; font-style: italic;">Learn how and when to remove this template message</a><span style="font-family: Verdana; font-size: 12pt; font-style: italic;">)</span></p></div>
</td></tr></table></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;"><br>Action research</span><span style="font-family: Verdana; font-size: 12pt;"> seeks transformative change through the simultaneous process of taking action and doing research, which are linked together by critical reflection.</span><a href="https://en.wikipedia.org/wiki/Kurt_Lewin" style="">Kurt Lewin</a><span style="font-family: Verdana; font-size: 12pt;">, then a professor at </span><a href="https://en.wikipedia.org/wiki/MIT" style="">MIT</a><span style="font-family: Verdana; font-size: 12pt;">, first coined the term "action research" in 1944. In his 1946 paper "Action Research and Minority Problems" he described action research as "a comparative research on the conditions and effects of various forms of social action and research leading to social action" that uses "a spiral of steps, each of which is composed of a circle of planning, action and fact-finding about the result of the action". </span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">Action research practitioners reflect upon the consequences of their own questions, beliefs, assumptions, and practices with the goal of understanding, developing, and improving social practices.</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-1" style="">[1]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;"> This action is designed to create three levels of change</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-2" style="">[2]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;"> (1) self-change as the only subject of action research is the person who conducting the research. This person is seeking to be better understand the effects of their action in social settings and to engage in a process of living his or her's values. The second level is a collective process of understanding change in a classroom, office, community, organization or institution. Action research enlists others and works to create a democratic sharing of voice to achieve deeper understanding of collective actions</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-3" style="">[3]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;">. Finally action research is process of sharing finding with the community of researchers. This can be done is many ways, in journals</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-4" style="">[4]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;">, on websites, in books, videos or at conferences. The Social Publishers Foundation</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-5" style="">[5]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;"> provides support for this action research process. </span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;"><br>Action research involves actively participating in a change situation, often via an existing organization, whilst simultaneously conducting research. Action research can also be undertaken by larger organizations or institutions, assisted or guided by professional researchers, with the aim of improving their strategies, practices and knowledge of the environments within which they practice. As designers and stakeholders, researchers work with others to propose a new course of action to help their community improve its work practices. Depending upon the nature of the people involved in the action research as well as the person(s) organizing it, there are different ways of describing action research</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-6" style="">[6]</a></p></div><ul class="list-0" style="left: -10px; position: relative;"><li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Collaborative Action Research</span></li>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt; font-weight: bold;">Action research</span><span style="font-family: Verdana; font-size: 12pt;"> seeks transformative change through the simultaneous process of taking action and doing research, which are linked together by critical reflection.</span><a href="https://en.wikipedia.org/wiki/Kurt_Lewin" style="font-family: Verdana; font-size: 12pt;">Kurt Lewin</a><span style="font-family: Verdana; font-size: 12pt;">, then a professor at </span><a href="https://en.wikipedia.org/wiki/MIT" style="font-family: Verdana; font-size: 12pt;">MIT</a><span style="font-family: Verdana; font-size: 12pt;">, first coined the term &quot;action research&quot; in 1944. In his 1946 paper &quot;Action Research and Minority Problems&quot; he described action research as &quot;a comparative research on the conditions and effects of various forms of social action and research leading to social action&quot; that uses &quot;a spiral of steps, each of which is composed of a circle of planning, action and fact-finding about the result of the action&quot;. </span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">Action research practitioners reflect upon the consequences of their own questions, beliefs, assumptions, and practices with the goal of understanding, developing, and improving social practices.</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-1" style="font-family: Verdana; font-size: 12pt; vertical-align: super;">[1]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;"> This action is designed to create three levels of change</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-2" style="font-family: Verdana; font-size: 12pt; vertical-align: super;">[2]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;"> (1) self-change as the only subject of action research is the person who conducting the research. This person is seeking to be better understand the effects of their action in social settings and to engage in a process of living his or her&apos;s values. The second level is a collective process of understanding change in a classroom, office, community, organization or institution. Action research enlists others and works to create a democratic sharing of voice to achieve deeper understanding of collective actions</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-3" style="font-family: Verdana; font-size: 12pt; vertical-align: super;">[3]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;">. Finally action research is process of sharing finding with the community of researchers. This can be done is many ways, in journals</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-4" style="font-family: Verdana; font-size: 12pt; vertical-align: super;">[4]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;">, on websites, in books, videos or at conferences. The Social Publishers Foundation</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-5" style="font-family: Verdana; font-size: 12pt; vertical-align: super;">[5]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;"> provides support for this action research process. </span></p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">Action research involves actively participating in a change situation, often via an existing organization, whilst simultaneously conducting research. Action research can also be undertaken by larger organizations or institutions, assisted or guided by professional researchers, with the aim of improving their strategies, practices and knowledge of the environments within which they practice. As designers and stakeholders, researchers work with others to propose a new course of action to help their community improve its work practices. Depending upon the nature of the people involved in the action research as well as the person(s) organizing it, there are different ways of describing action research</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-6" style="font-family: Verdana; font-size: 12pt; vertical-align: super;">[6]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;">. </span></p></div><ul class="list-0" style="left: -10px; position: relative;"><li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Collaborative Action Research</span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Participatory Action Research</span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Community-Based Action Research</span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Youth Action Research</span></li>
@@ -808,13 +846,13 @@ exports[`InteropService_Importer_OneNote should ignore broken characters at the
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Action Science</span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Living Theory Action Research</span></li>
</ul>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;"><br>There are also a set of approaches that share some properties with action research but have some different practices</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-7" style="">[7]</a></p></div><ul class="list-1" style="left: -10px; position: relative;"><li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Appreciative Inquiry is a way of starting with what is working well and then using action research to improve it.</span></li>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><span style="font-family: Verdana; font-size: 12pt;">There are also a set of approaches that share some properties with action research but have some different practices</span><a href="https://en.wikipedia.org/wiki/Action_research#cite_note-7" style="font-family: Verdana; font-size: 12pt; vertical-align: super;">[7]</a><span style="font-family: Verdana; font-size: 12pt; vertical-align: super;">. These include: </span></p></div><ul class="list-1" style="left: -10px; position: relative;"><li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Appreciative Inquiry is a way of starting with what is working well and then using action research to improve it.</span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Calibri; font-size: 11pt;"><span style="font-family: Verdana; font-size: 12pt; font-style: italic;">Lesson Study</span><span style="font-family: Verdana; font-size: 12pt;"> places the teaching of a shared lesson as the action and has a set of protocols for understanding the outcomes.</span></span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Calibri; font-size: 11pt;"><span style="font-family: Verdana; font-size: 12pt; font-style: italic;">Practitioner Research</span><span style="font-family: Verdana; font-size: 12pt;"> does not have to be action research, as practitioners can engage in any form of the many forms of research.</span></span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Calibri; font-size: 11pt;"><span style="font-family: Verdana; font-size: 12pt; font-style: italic;">Reflective Practice/Self Study</span><span style="font-family: Verdana; font-size: 12pt;"> is the first part of action research but does not require the practitioner to make the results public, to share the results of the learning with others.  Many of these approaches will be described in these resources.</span></span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Calibri; font-size: 11pt;"><span style="font-family: Verdana; font-size: 12pt; font-style: italic;">Teacher Research</span><span style="font-family: Verdana; font-size: 12pt;"> can be any form of research that teachers do, including action research, but not limited to it. At George Mason University, teacher research is described in a way that is very similar to what most authors understand as action research. And at some point, they suggest that action research can be a synonym of teacher research.  The description of action research posted on this site is more closely aligned to what we have called reflective practice.   This shows the variation in the way that people working in the field have of conceptualizing these terms.</span></span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Action Inquiry draws on action research and recasts evaluation research to help navigate complexity when enacting collective leadership. Find out more about by reading this document from Scotland.</span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Improvement Science is explicitly designed to accelerate learning-by-doing. It's a more user-centered and problem-centered approached to improving teaching and learning that is highly similar to action research supported by the Carnegie Foundation for the Advancement of Teaching.</span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Verdana; font-size: 12pt;">Improvement Science is explicitly designed to accelerate learning-by-doing. It&apos;s a more user-centered and problem-centered approached to improving teaching and learning that is highly similar to action research supported by the Carnegie Foundation for the Advancement of Teaching.</span></li>
</ul>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; padding-bottom: 16px; padding-top: 7px;"><br></p></div>
</div>
@@ -982,7 +1020,7 @@ exports[`InteropService_Importer_OneNote should remove hyperlink from title: Tip
<div class="title" style="left: 48px; position: absolute; top: 24px;"><div class="container-outline" style="width: 624px;"><div class="outline-element" style="margin-left: 0px;"><span style="font-family: Calibri Light; font-size: 20pt; line-height: 32px;">&nbsp;</span></div>
</div><div class="container-outline" style="width: 624px;"><div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(118,118,118); font-family: Calibri; font-size: 10pt; line-height: 16px;">Saturday, February 11, 2023</span></div>
<div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(118,118,118); font-family: Calibri; font-size: 10pt; line-height: 16px;">12:56 AM</span></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 115px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 14pt; line-height: 22px;"><a href="onenote:https://d.docs.live.net/c8d3bbab7f1acf3a/Documents/Photography/风景.one#Tips%20from%20a%20Pro%20Using%20Trees%20for%20Dramatic%20Landscape%20Photography&section-id={262ADDFB-A4DC-4453-A239-0024D6769962}&page-id={88D803A5-4F43-48D4-9B16-4C024F5787DC}&end" style="">Tips from a Pro: Using Trees for Dramatic Landscape Photography</a></p></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 115px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 14pt; line-height: 22px;"><a href="onenote:https://d.docs.live.net/c8d3bbab7f1acf3a/Documents/Photography/%E9%A3%8E%E6%99%AF.one#Tips%20from%20a%20Pro%20Using%20Trees%20for%20Dramatic%20Landscape%20Photography&section-id={262ADDFB-A4DC-4453-A239-0024D6769962}&page-id={88D803A5-4F43-48D4-9B16-4C024F5787DC}&end" style="">Tips from a Pro: Using Trees for Dramatic Landscape Photography</a></p></div>
</div>
<script>
@@ -1044,7 +1082,7 @@ exports[`InteropService_Importer_OneNote should remove hyperlink from title: 风
<div class="title" style="left: 48px; position: absolute; top: 24px;"><div class="container-outline" style="width: 624px;"><div class="outline-element" style="margin-left: 0px;"><span style="font-family: Calibri Light; font-size: 20pt;">&nbsp;</span></div>
</div><div class="container-outline" style="width: 624px;"><div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(128,128,128); font-family: Calibri; font-size: 10pt;">Sunday, January 5, 2025</span></div>
<div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(128,128,128); font-family: Calibri; font-size: 10pt;">10:13 PM</span></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 115px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt;"><a href="onenote:#风景&section-id={75256889-9e75-4ec2-82ed-fc799557e1b9}&page-id={d099b6f3-7f5a-4c08-aed7-e8d42c59523f}&end" style="">风景</a><span style="font-family: Calibri; font-size: 11pt;"> (</span><a href="https://onedrive.live.com/edit.aspx?resid=193EE54E3252492D!s9b62db4219f740709f444bc0129de4e9&migratedtospo=true&wd=target%28Quick%20Notes.one%7C75256889-9e75-4ec2-82ed-fc799557e1b9%2F%E9%A3%8E%E6%99%AF%7Cd099b6f3-7f5a-4c08-aed7-e8d42c59523f%2F%29&wdorigin=703&wdpreservelink=1" style="">Web view</a><span style="font-family: Calibri; font-size: 11pt;">)</span></p></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 115px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt;"><a href="onenote:#%E9%A3%8E%E6%99%AF&section-id={75256889-9e75-4ec2-82ed-fc799557e1b9}&page-id={d099b6f3-7f5a-4c08-aed7-e8d42c59523f}&end" style="font-family: Calibri; font-size: 11pt;">风景</a><span style="font-family: Calibri; font-size: 11pt;"> (</span><a href="https://onedrive.live.com/edit.aspx?resid=193EE54E3252492D!s9b62db4219f740709f444bc0129de4e9&migratedtospo=true&wd=target%28Quick%20Notes.one%7C75256889-9e75-4ec2-82ed-fc799557e1b9%2F%E9%A3%8E%E6%99%AF%7Cd099b6f3-7f5a-4c08-aed7-e8d42c59523f%2F%29&wdorigin=703&wdpreservelink=1" style="font-family: Calibri; font-size: 11pt;">Web view</a><span style="font-family: Calibri; font-size: 11pt;">)</span></p></div>
</div>
<script>
@@ -1381,7 +1419,7 @@ exports[`InteropService_Importer_OneNote should support importing .one files tha
</span>Rédiger carnet MS-OneNote jeu d'essai (case cochée)</span></li>
<li class="outline-element" style="margin-left: 0px;"><span style="font-family: Calibri; font-size: 11pt; line-height: 17px;"><span aria-checked="false" aria-disabled="true" class="note-tag-icon icon-1 -checkbox -large" role="checkbox"><img class="checkbox-icon" alt="Unchecked" src=":/id-here">
</span>Transmettre aux dev Joplin (case non cochée)</span><ul class="tagged-list"><li class="outline-element" style="margin-left: 36px;"><span style="font-family: Calibri; font-size: 11pt; line-height: 17px;"><span aria-checked="true" aria-disabled="true" class="note-tag-icon icon-2 -checkbox -large" role="checkbox"><img alt="Checked" src=":/id-here">
</span>Exporter le carnet en *.packageone (case cochée)</span></li>
</span>Exporter le carnet en *.onepkg (case cochée)</span></li>
<li class="outline-element" style="margin-left: 36px;"><span style="font-family: Calibri; font-size: 11pt; line-height: 17px;"><span aria-checked="false" aria-disabled="true" class="note-tag-icon icon-3 -checkbox -large" role="checkbox"><img class="checkbox-icon" alt="Unchecked" src=":/id-here">
</span>Exporter des page en *.one (case non cochée)</span></li>
</ul></li>
@@ -1459,7 +1497,7 @@ exports[`InteropService_Importer_OneNote should use default value for EntityGuid
<div class="title" style="left: 48px; position: absolute; top: 24px;"><div class="container-outline"><div class="outline-element" style="margin-left: 0px;"><span style="font-family: Calibri Light; font-size: 20pt; line-height: 32px;">Decrease support costs</span></div>
</div><div class="container-outline"><div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(128,128,128); font-family: Calibri; font-size: 10pt; line-height: 16px;">Saturday, October 10, 2015</span></div>
<div class="outline-element" style="margin-left: 0px;"><span style="color: rgb(128,128,128); font-family: Calibri; font-size: 10pt; line-height: 16px;">11:15 PM</span></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 88px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; line-height: 17px;">One of the strategic goals of training <span style="font-style: italic; font-weight: bold;">must</span> be to decrease the cost of customer support. To do this training must teach customers to "<span style="font-weight: bold;">do</span>" not to "know." How many customers call asking to know something?</p></div>
</div></div><div class="container-outline" style="left: 48px; position: absolute; top: 88px; width: 624px;"><div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; line-height: 17px;">One of the strategic goals of training <span style="font-style: italic; font-weight: bold;">must</span> be to decrease the cost of customer support. To do this training must teach customers to &quot;<span style="font-weight: bold;">do</span>&quot; not to &quot;know.&quot; How many customers call asking to know something?</p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; line-height: 17px;">&nbsp;</p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; line-height: 17px;">&nbsp;</p></div>
<div class="outline-element" style="margin-left: 0px;"><p style="font-family: Calibri; font-size: 11pt; line-height: 17px;">&nbsp;</p></div>

View File

@@ -101,7 +101,11 @@ export const checkIfLoginWasSuccessful = async (applicationsUrl: string) => {
if (isWaitingResponse) return undefined;
isWaitingResponse = true;
const response = await fetch(applicationsUrl);
const response = await fetch(applicationsUrl, {
headers: {
'X-JOPLIN-CUSTOM-API-KEY': '',
},
});
const jsonBody = await response.json();
if (!response.ok || jsonBody.status !== 'finished') {

View File

@@ -54,6 +54,16 @@ export interface ShowMessageBoxOptions {
cancelId?: number;
}
export enum ToastType {
Info = 'info',
Error = 'error',
Success = 'success',
}
export interface ShowToastOptions {
type: ToastType;
}
export enum MobilePlatform {
None = '',
Android = 'android',
@@ -458,6 +468,11 @@ const shim = {
return await shim.showMessageBox(message, { type: MessageBoxType.Confirm }) === 0;
},
showToast: async (message: string, { type = ToastType.Info }: ShowToastOptions = null): Promise<void> => {
// Should usually be overridden by implementers
await shim.showMessageBox(message, { type: type === ToastType.Error ? MessageBoxType.Error : MessageBoxType.Info });
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
writeImageToFile: (_image: any, _format: any, _filePath: string): void => {
throw new Error('Not implemented');

View File

@@ -95,6 +95,14 @@ class Time {
);
}
public rfc3339SecToUnixMs(rfc3339: string): number {
const m = moment.utc(rfc3339, 'YYYY-MM-DD HH:mm:ss[Z]', true);
if (!m.isValid()) {
throw new Error(`Invalid RFC3339 date format: ${rfc3339}`);
}
return m.valueOf();
}
public unixMsToLocalDateTime(ms: number): string {
return moment.unix(ms / 1000).format('DD/MM/YYYY HH:mm');
}

View File

@@ -3,6 +3,7 @@ import { NoteEntity } from '../services/database/types';
import { MdFrontMatterExport } from '../services/interop/types';
import time from '../time';
import * as yaml from 'js-yaml';
const moment = require('moment');
export interface ParsedMeta {
metadata: NoteEntity;
@@ -13,6 +14,19 @@ const convertDate = (datetime: number): string => {
return time.unixMsToRfc3339Sec(datetime);
};
const dateStringToDate = (dateString: string, defaultValue: number) => {
try {
// When exporting with Joplin, we encode the date in this format, so try this first.
const ms = time.rfc3339SecToUnixMs(dateString);
return ms;
} catch {
// If it fails, try to parse with `moment`:
const m = moment(dateString);
return m.isValid() ? m.toDate().getTime() : defaultValue;
}
};
export const fieldOrder = ['title', 'id', 'updated', 'created', 'source', 'author', 'latitude', 'longitude', 'altitude', 'completed?', 'due', 'tags'];
// There is a special case (negative numbers) where the yaml library will force quotations
@@ -200,24 +214,24 @@ export const parse = (note: string): ParsedMeta => {
// The date fallback gives support for MultiMarkdown format, r-markdown, and pandoc formats
if ('created' in md) {
metadata['user_created_time'] = time.anythingToMs(md['created'], Date.now());
metadata['user_created_time'] = dateStringToDate(md['created'], Date.now());
} else if ('date' in md) {
metadata['user_created_time'] = time.anythingToMs(md['date'], Date.now());
metadata['user_created_time'] = dateStringToDate(md['date'], Date.now());
} else if ('created_at' in md) {
// Add support for Notesnook
metadata['user_created_time'] = time.anythingToMs(md['created_at'], Date.now());
metadata['user_created_time'] = dateStringToDate(md['created_at'], Date.now());
}
if ('updated' in md) {
metadata['user_updated_time'] = time.anythingToMs(md['updated'], Date.now());
metadata['user_updated_time'] = dateStringToDate(md['updated'], Date.now());
} else if ('lastmod' in md) {
// Add support for hugo
metadata['user_updated_time'] = time.anythingToMs(md['lastmod'], Date.now());
metadata['user_updated_time'] = dateStringToDate(md['lastmod'], Date.now());
} else if ('date' in md) {
metadata['user_updated_time'] = time.anythingToMs(md['date'], Date.now());
metadata['user_updated_time'] = dateStringToDate(md['date'], Date.now());
} else if ('updated_at' in md) {
// Notesnook
metadata['user_updated_time'] = time.anythingToMs(md['updated_at'], Date.now());
metadata['user_updated_time'] = dateStringToDate(md['updated_at'], Date.now());
}
if ('latitude' in md) { metadata['latitude'] = md['latitude']; }
@@ -230,7 +244,7 @@ export const parse = (note: string): ParsedMeta => {
metadata['todo_completed'] = metadata['user_updated_time'] ?? Date.now();
}
if ('due' in md) {
const due_date = time.anythingToMs(md['due'], null);
const due_date = dateStringToDate(md['due'], null);
if (due_date) { metadata['todo_due'] = due_date; }
}
}
@@ -240,8 +254,10 @@ export const parse = (note: string): ParsedMeta => {
if ('tags' in md) {
// Only create unique tags
tags = md['tags'];
} else if ('keywords' in md) {
// Adding support for r-markdown/pandoc
} else if ('keywords' in md && Array.isArray(md.keywords)) {
// Support for r-markdown/pandoc. Note that "keywords" may be an empty field in the input
// document, which would be parsed as just "null", so this is why we need to check that it
// is an array. Fixes: https://github.com/laurent22/joplin/issues/13008
tags = tags.concat(md['keywords']);
}

View File

@@ -369,7 +369,6 @@ dependencies = [
"palette",
"parser-macros",
"parser-utils",
"percent-encoding",
"regex",
"sanitize-filename",
"uuid",
@@ -568,6 +567,7 @@ dependencies = [
"parser",
"parser-utils",
"paste",
"percent-encoding",
"regex",
"sanitize-filename",
"thiserror",

View File

@@ -13,7 +13,6 @@ color-eyre = "0.5"
log = "0.4.11"
mime_guess = "2.0.3"
palette = "0.5.0"
percent-encoding = "2.1.0"
regex = "1"
sanitize-filename = "0.3.0"
console_error_panic_hook = "0.1.7"

View File

@@ -67,5 +67,8 @@ pub mod property {
pub mod rich_text {
pub use crate::one::property::paragraph_alignment::ParagraphAlignment;
pub use crate::onenote::rich_text::ParagraphStyling;
pub use crate::onenote::text_region::Hyperlink;
pub use crate::onenote::text_region::MathExpression;
pub use crate::onenote::text_region::TextRegion;
}
}

View File

@@ -94,7 +94,12 @@ impl crate::onestore::object_space::ObjectSpace for ObjectSpace {
self.revision_list
.revisions
.iter()
.rev()
// TODO: It would make more sense to use the **last** revision, rather than
// the first to get the content root. However, doing so seems to return
// version history information, rather than the true content root.
// In the future, if there are issues related to importing the wrong versions
// of pages, look into this.
// .rev()
.find_map(|revision| revision.content_root())
}
@@ -102,7 +107,7 @@ impl crate::onestore::object_space::ObjectSpace for ObjectSpace {
self.revision_list
.revisions
.iter()
.rev()
// .rev() // TODO: Why does calling .rev() result in the wrong metadata being returned?
.find_map(|revision| revision.metadata_root())
}
}

View File

@@ -29,6 +29,7 @@ pub struct Revision {
root_objects: HashMap<RootRole, ExGuid>,
}
// See [MS-ONE 2.1.8](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-one/037e31c0-4484-4a14-819a-0ddece2cacbc)
#[derive(Eq, PartialEq, Hash, Debug)]
pub enum RootRole {
DefaultContent,
@@ -95,6 +96,7 @@ impl Revision {
let mut last_index = iterator.get_index();
while let Some(current) = iterator.peek() {
if let FileNodeData::RevisionManifestEndFND = current {
iterator.next();
break;
} else if let Some(object_group_list) = ObjectGroupList::try_parse(iterator, context)? {
// Skip: Used for reference counting (which we can ignore here)

View File

@@ -205,4 +205,7 @@ pub(crate) enum PropertyType {
EmbeddedInkSpaceHeight = 0x14001C28,
ImageEmbedType = 0x140035F2,
ImageEmbeddedUrl = 0x1C0035F3,
MathUnknown1 = 0x10003453, // Unknown 16-bit math-related property (operator variant?)
MathOperator = 0x1400344f,
}

View File

@@ -1,6 +1,7 @@
use crate::one::property::PropertyType;
use crate::onestore::object::Object;
use crate::shared::guid::Guid;
use crate::shared::prop_set::PropertySet;
use encoding_rs::mem::decode_latin1;
use parser_utils::Utf16ToString;
use parser_utils::errors::{ErrorKind, Result};
@@ -158,3 +159,24 @@ pub(crate) fn parse_guid(prop_type: PropertyType, object: &Object) -> Result<Opt
Ok(Some(Guid::parse(&mut Reader::new(data))?))
}
pub(crate) fn parse_property_values(
prop_type: PropertyType,
object: &Object,
) -> Result<Option<&[PropertySet]>> {
let value = match object.props().get(prop_type) {
Some(value) => Some(
value
.to_property_values()
.ok_or_else(|| {
parser_error!(
MalformedOneNoteFileData,
"PropertyValue value is not a PropertyValue"
)
})?
.1,
),
None => None,
};
Ok(value)
}

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