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

Compare commits

..

8 Commits

Author SHA1 Message Date
Laurent Cozic
b519d55abf Merge branch 'dev' into table_editor 2022-06-14 00:43:23 +01:00
Laurent Cozic
5a862443d8 Merge branch 'dev' into table_editor 2022-06-07 18:30:40 +01:00
Laurent
30e191663d Merge branch 'dev' into table_editor 2022-04-17 12:42:46 +01:00
Laurent
70cd2395fb Merge branch 'dev' into table_editor 2022-04-12 23:31:35 +01:00
Laurent
2f1b6fbee1 Merge branch 'dev' into table_editor 2022-04-12 15:25:01 +01:00
Laurent
8c0d4a0f71 Merge branch 'dev' into table_editor 2022-04-11 20:04:24 +01:00
Laurent
bc08c6dcc3 Merge branch 'dev' into table_editor 2022-04-05 19:17:37 +01:00
Laurent Cozic
a06365039d table editor init 2022-03-07 17:17:41 +00:00
277 changed files with 3607 additions and 14802 deletions

View File

@@ -46,7 +46,7 @@ packages/app-desktop/packageInfo.js
packages/app-desktop/services/electron-context-menu.js
packages/app-desktop/vendor/lib/
packages/app-mobile/android
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.bundle.js
packages/app-mobile/components/NoteEditor/CodeMirror.bundle.js
packages/app-mobile/ios
packages/app-mobile/lib/rnInjectedJs/
packages/app-mobile/locales
@@ -69,7 +69,6 @@ packages/tools/node_modules
packages/tools/PortableAppsLauncher
packages/turndown-plugin-gfm/
packages/turndown/
packages/pdf-viewer/dist
plugin_types/
readme/
@@ -422,6 +421,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
@@ -689,6 +691,9 @@ packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/SyncWizard/Dialog.js.map
packages/app-desktop/gui/TableEditorDialog/Dialog.d.ts
packages/app-desktop/gui/TableEditorDialog/Dialog.js
packages/app-desktop/gui/TableEditorDialog/Dialog.js.map
packages/app-desktop/gui/TagList.d.ts
packages/app-desktop/gui/TagList.js
packages/app-desktop/gui/TagList.js.map
@@ -854,69 +859,12 @@ packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js.ma
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.d.ts
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/createEditor.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/createEditor.js
packages/app-mobile/components/NoteEditor/CodeMirror/createEditor.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/theme.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/types.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/types.js
packages/app-mobile/components/NoteEditor/CodeMirror/types.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js.map
packages/app-mobile/components/NoteEditor/EditLinkDialog.d.ts
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
packages/app-mobile/components/NoteEditor/EditLinkDialog.js.map
packages/app-mobile/components/NoteEditor/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
packages/app-mobile/components/NoteEditor/SearchPanel.d.ts
packages/app-mobile/components/NoteEditor/SearchPanel.js
packages/app-mobile/components/NoteEditor/SearchPanel.js.map
packages/app-mobile/components/NoteEditor/SelectionFormatting.d.ts
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
packages/app-mobile/components/NoteEditor/SelectionFormatting.js.map
packages/app-mobile/components/NoteEditor/types.d.ts
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteEditor/types.js.map
packages/app-mobile/components/SelectDateTimeDialog.d.ts
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SelectDateTimeDialog.js.map
@@ -932,9 +880,6 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
packages/app-mobile/components/screens/encryption-config.d.ts
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/encryption-config.js.map
packages/app-mobile/gulpfile.d.ts
packages/app-mobile/gulpfile.js
packages/app-mobile/gulpfile.js.map
packages/app-mobile/root.d.ts
packages/app-mobile/root.js
packages/app-mobile/root.js.map
@@ -950,9 +895,6 @@ packages/app-mobile/services/e2ee/RSA.react-native.js.map
packages/app-mobile/setupQuickActions.d.ts
packages/app-mobile/setupQuickActions.js
packages/app-mobile/setupQuickActions.js.map
packages/app-mobile/tools/buildInjectedJs.d.ts
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/tools/buildInjectedJs.js.map
packages/app-mobile/utils/ShareExtension.d.ts
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareExtension.js.map
@@ -1052,12 +994,6 @@ packages/lib/ClipperServer.js.map
packages/lib/CssUtils.d.ts
packages/lib/CssUtils.js
packages/lib/CssUtils.js.map
packages/lib/EventDispatcher.d.ts
packages/lib/EventDispatcher.js
packages/lib/EventDispatcher.js.map
packages/lib/EventDispatcher.test.d.ts
packages/lib/EventDispatcher.test.js
packages/lib/EventDispatcher.test.js.map
packages/lib/HtmlToMd.d.ts
packages/lib/HtmlToMd.js
packages/lib/HtmlToMd.js.map
@@ -1559,9 +1495,6 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js.map
packages/lib/services/keychain/KeychainServiceDriverBase.d.ts
packages/lib/services/keychain/KeychainServiceDriverBase.js
packages/lib/services/keychain/KeychainServiceDriverBase.js.map
packages/lib/services/plugins/BasePlatformImplementation.d.ts
packages/lib/services/plugins/BasePlatformImplementation.js
packages/lib/services/plugins/BasePlatformImplementation.js.map
packages/lib/services/plugins/BasePluginRunner.d.ts
packages/lib/services/plugins/BasePluginRunner.js
packages/lib/services/plugins/BasePluginRunner.js.map
@@ -1952,27 +1885,6 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/pdf-viewer/Page.d.ts
packages/pdf-viewer/Page.js
packages/pdf-viewer/Page.js.map
packages/pdf-viewer/VerticalPages.d.ts
packages/pdf-viewer/VerticalPages.js
packages/pdf-viewer/VerticalPages.js.map
packages/pdf-viewer/hooks/useIsFocused.d.ts
packages/pdf-viewer/hooks/useIsFocused.js
packages/pdf-viewer/hooks/useIsFocused.js.map
packages/pdf-viewer/hooks/useIsVisible.d.ts
packages/pdf-viewer/hooks/useIsVisible.js
packages/pdf-viewer/hooks/useIsVisible.js.map
packages/pdf-viewer/miniViewer.d.ts
packages/pdf-viewer/miniViewer.js
packages/pdf-viewer/miniViewer.js.map
packages/pdf-viewer/pdfSource.d.ts
packages/pdf-viewer/pdfSource.js
packages/pdf-viewer/pdfSource.js.map
packages/pdf-viewer/pdfSource.test.d.ts
packages/pdf-viewer/pdfSource.test.js
packages/pdf-viewer/pdfSource.test.js.map
packages/plugin-repo-cli/commands/updateRelease.d.ts
packages/plugin-repo-cli/commands/updateRelease.js
packages/plugin-repo-cli/commands/updateRelease.js.map
@@ -2024,9 +1936,6 @@ packages/plugins/ToggleSidebars/api/types.js.map
packages/plugins/ToggleSidebars/src/index.d.ts
packages/plugins/ToggleSidebars/src/index.js
packages/plugins/ToggleSidebars/src/index.js.map
packages/react-native-saf-x/src/index.d.ts
packages/react-native-saf-x/src/index.js
packages/react-native-saf-x/src/index.js.map
packages/renderer/HtmlToHtml.d.ts
packages/renderer/HtmlToHtml.js
packages/renderer/HtmlToHtml.js.map

View File

@@ -76,7 +76,6 @@ module.exports = {
'no-array-constructor': ['error'],
'radix': ['error'],
'eqeqeq': ['error', 'always'],
// Warn only for now because fixing everything would take too much
// refactoring, but new code should try to stick to it.

View File

@@ -57,11 +57,6 @@ echo "Yarn $( yarn -v )"
cd "$ROOT_DIR"
yarn install
testResult=$?
if [ $testResult -ne 0 ]; then
echo "Yarn installation failed. Search for 'exit code 1' in the log for more information."
exit $testResult
fi
# =============================================================================
# Run test units. Only do it for pull requests and dev branch because we don't

108
.gitignore vendored
View File

@@ -411,6 +411,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
@@ -678,6 +681,9 @@ packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/SyncWizard/Dialog.js.map
packages/app-desktop/gui/TableEditorDialog/Dialog.d.ts
packages/app-desktop/gui/TableEditorDialog/Dialog.js
packages/app-desktop/gui/TableEditorDialog/Dialog.js.map
packages/app-desktop/gui/TagList.d.ts
packages/app-desktop/gui/TagList.js
packages/app-desktop/gui/TagList.js.map
@@ -843,69 +849,12 @@ packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js.ma
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.d.ts
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/createEditor.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/createEditor.js
packages/app-mobile/components/NoteEditor/CodeMirror/createEditor.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/theme.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/types.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/types.js
packages/app-mobile/components/NoteEditor/CodeMirror/types.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js.map
packages/app-mobile/components/NoteEditor/EditLinkDialog.d.ts
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
packages/app-mobile/components/NoteEditor/EditLinkDialog.js.map
packages/app-mobile/components/NoteEditor/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
packages/app-mobile/components/NoteEditor/SearchPanel.d.ts
packages/app-mobile/components/NoteEditor/SearchPanel.js
packages/app-mobile/components/NoteEditor/SearchPanel.js.map
packages/app-mobile/components/NoteEditor/SelectionFormatting.d.ts
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
packages/app-mobile/components/NoteEditor/SelectionFormatting.js.map
packages/app-mobile/components/NoteEditor/types.d.ts
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteEditor/types.js.map
packages/app-mobile/components/SelectDateTimeDialog.d.ts
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SelectDateTimeDialog.js.map
@@ -921,9 +870,6 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
packages/app-mobile/components/screens/encryption-config.d.ts
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/encryption-config.js.map
packages/app-mobile/gulpfile.d.ts
packages/app-mobile/gulpfile.js
packages/app-mobile/gulpfile.js.map
packages/app-mobile/root.d.ts
packages/app-mobile/root.js
packages/app-mobile/root.js.map
@@ -939,9 +885,6 @@ packages/app-mobile/services/e2ee/RSA.react-native.js.map
packages/app-mobile/setupQuickActions.d.ts
packages/app-mobile/setupQuickActions.js
packages/app-mobile/setupQuickActions.js.map
packages/app-mobile/tools/buildInjectedJs.d.ts
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/tools/buildInjectedJs.js.map
packages/app-mobile/utils/ShareExtension.d.ts
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareExtension.js.map
@@ -1041,12 +984,6 @@ packages/lib/ClipperServer.js.map
packages/lib/CssUtils.d.ts
packages/lib/CssUtils.js
packages/lib/CssUtils.js.map
packages/lib/EventDispatcher.d.ts
packages/lib/EventDispatcher.js
packages/lib/EventDispatcher.js.map
packages/lib/EventDispatcher.test.d.ts
packages/lib/EventDispatcher.test.js
packages/lib/EventDispatcher.test.js.map
packages/lib/HtmlToMd.d.ts
packages/lib/HtmlToMd.js
packages/lib/HtmlToMd.js.map
@@ -1548,9 +1485,6 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js.map
packages/lib/services/keychain/KeychainServiceDriverBase.d.ts
packages/lib/services/keychain/KeychainServiceDriverBase.js
packages/lib/services/keychain/KeychainServiceDriverBase.js.map
packages/lib/services/plugins/BasePlatformImplementation.d.ts
packages/lib/services/plugins/BasePlatformImplementation.js
packages/lib/services/plugins/BasePlatformImplementation.js.map
packages/lib/services/plugins/BasePluginRunner.d.ts
packages/lib/services/plugins/BasePluginRunner.js
packages/lib/services/plugins/BasePluginRunner.js.map
@@ -1941,27 +1875,6 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/pdf-viewer/Page.d.ts
packages/pdf-viewer/Page.js
packages/pdf-viewer/Page.js.map
packages/pdf-viewer/VerticalPages.d.ts
packages/pdf-viewer/VerticalPages.js
packages/pdf-viewer/VerticalPages.js.map
packages/pdf-viewer/hooks/useIsFocused.d.ts
packages/pdf-viewer/hooks/useIsFocused.js
packages/pdf-viewer/hooks/useIsFocused.js.map
packages/pdf-viewer/hooks/useIsVisible.d.ts
packages/pdf-viewer/hooks/useIsVisible.js
packages/pdf-viewer/hooks/useIsVisible.js.map
packages/pdf-viewer/miniViewer.d.ts
packages/pdf-viewer/miniViewer.js
packages/pdf-viewer/miniViewer.js.map
packages/pdf-viewer/pdfSource.d.ts
packages/pdf-viewer/pdfSource.js
packages/pdf-viewer/pdfSource.js.map
packages/pdf-viewer/pdfSource.test.d.ts
packages/pdf-viewer/pdfSource.test.js
packages/pdf-viewer/pdfSource.test.js.map
packages/plugin-repo-cli/commands/updateRelease.d.ts
packages/plugin-repo-cli/commands/updateRelease.js
packages/plugin-repo-cli/commands/updateRelease.js.map
@@ -2013,9 +1926,6 @@ packages/plugins/ToggleSidebars/api/types.js.map
packages/plugins/ToggleSidebars/src/index.d.ts
packages/plugins/ToggleSidebars/src/index.js
packages/plugins/ToggleSidebars/src/index.js.map
packages/react-native-saf-x/src/index.d.ts
packages/react-native-saf-x/src/index.js
packages/react-native-saf-x/src/index.js.map
packages/renderer/HtmlToHtml.d.ts
packages/renderer/HtmlToHtml.js
packages/renderer/HtmlToHtml.js.map

View File

@@ -9,13 +9,11 @@ import PluginManager from 'tinymce/core/api/PluginManager';
import * as Api from './api/Api';
import * as Commands from './api/Commands';
import * as Keyboard from './core/Keyboard';
import * as Mouse from './core/Mouse'
import * as Buttons from './ui/Buttons';
export default function () {
PluginManager.add('joplinLists', function (editor) {
Keyboard.setup(editor);
Mouse.setup(editor);
Buttons.register(editor);
Commands.register(editor);

View File

@@ -1,26 +0,0 @@
import { isJoplinChecklistItem } from '../listModel/JoplinListUtil';
const setup = function (editor) {
const editorClickHandler = (event) => {
if (!isJoplinChecklistItem(event.target)) return;
// We only process the click if it's within the checkbox itself (and not the label).
// That checkbox, based on
// the current styling is in the negative margin, so offsetX is negative when clicking
// on the checkbox itself, and positive when clicking on the label. This is strongly
// dependent on how the checkbox is styled, so if the style is changed, this might need
// to be updated too.
// For the styling, see:
// packages/renderer/MdToHtml/rules/checkbox.ts
//
// The previous solution was to use "pointer-event: none", which mostly work, however
// it means that links are no longer clickable when they are within the checkbox label.
if (event.offsetX >= 0) return;
editor.execCommand('ToggleJoplinChecklistItem', false, { element: event.target });
}
editor.on('click', editorClickHandler);
};
export { setup };

View File

@@ -10,7 +10,7 @@ import * as Settings from '../api/Settings';
import * as NodeType from '../core/NodeType';
import Editor from 'tinymce/core/api/Editor';
import { isCustomList } from '../core/Util';
import { findContainerListTypeFromEvent } from '../listModel/JoplinListUtil';
import { findContainerListTypeFromEvent, isJoplinChecklistItem } from '../listModel/JoplinListUtil';
const findIndex = function (list, predicate) {
for (let index = 0; index < list.length; index++) {
@@ -38,11 +38,37 @@ const listState = function (editor: Editor, listName, options:any = {}) {
buttonApi.setActive(listType === options.listType && lists.length > 0 && lists[0].nodeName === listName && !isCustomList(lists[0]));
};
const editorClickHandler = (event) => {
if (!isJoplinChecklistItem(event.target)) return;
// We only process the click if it's within the checkbox itself (and not the label).
// That checkbox, based on
// the current styling is in the negative margin, so offsetX is negative when clicking
// on the checkbox itself, and positive when clicking on the label. This is strongly
// dependent on how the checkbox is styled, so if the style is changed, this might need
// to be updated too.
// For the styling, see:
// packages/renderer/MdToHtml/rules/checkbox.ts
//
// The previous solution was to use "pointer-event: none", which mostly work, however
// it means that links are no longer clickable when they are within the checkbox label.
if (event.offsetX >= 0) return;
editor.execCommand('ToggleJoplinChecklistItem', false, { element: event.target });
}
if (options.listType === 'joplinChecklist') {
editor.on('click', editorClickHandler);
}
editor.on('NodeChange', nodeChangeHandler);
return () => {
if (options.listType === 'joplinChecklist') {
editor.off('click', editorClickHandler);
}
editor.off('NodeChange', nodeChangeHandler);
}
}
};
};

View File

@@ -1,9 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 08 Aug 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
<p>This is an opportunity to meet other Joplin users as well as some of the main contributors, to discuss the apps, or to ask questions and exchange tips and tricks on how to use the app, develop plugins or contribute to the application. Everybody, technical or not, is welcome!</p>
<p>We will meet at the Old Thameside Inn next to London Bridge. If the weather allows we will be on the terrace outside, if not inside.</p>
<p>More information on the official Meetup page:</p>
<p><a href="https://www.meetup.com/joplin/events/287611873/">https://www.meetup.com/joplin/events/287611873/</a></p>
]]></description><link>https://joplinapp.org/news/20220808-first-meetup/</link><guid isPermaLink="false">20220808-first-meetup</guid><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><twitter-text>Joplin will have its first Meetup on 30 August! Come and join us at the Old Thameside Inn next to London Bridge! https://www.meetup.com/joplin/events/287611873/</twitter-text></item><item><title><![CDATA[Joplin 2.8 is available!]]></title><description><![CDATA[<p>As always a lot of changes and new features in this new version available on both desktop and mobile.</p>
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 06 Jun 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 06 Jun 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin 2.8 is available!]]></title><description><![CDATA[<p>As always a lot of changes and new features in this new version available on both desktop and mobile.</p>
<h1>Multiple profile support<a name="multiple-profile-support" href="#multiple-profile-support" class="heading-anchor">🔗</a></h1>
<p>Perhaps the most visible change in this version is the support for multiple profiles. You can now create as many application profile as you wish, each with their own settings, and easily switch from one to another. The main use case is to support for example a &quot;work&quot; profile and a &quot;personal&quot; profile, to allow you to keep things independent, and each profile can sync with a different sync target.</p>
<p>To create a new profile, open <strong>File &gt; Switch profile</strong> and select <strong>Create new profile</strong>, enter the profile name and press OK. The app will automatically switch to this new profile, which you can now configure.</p>
@@ -261,4 +256,6 @@
]]></description><link>https://joplinapp.org/news/20210706-140228/</link><guid isPermaLink="false">20210706-140228</guid><pubDate>Tue, 06 Jul 2021 14:02:28 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Any ideas for a Joplin tagline?]]></title><description><![CDATA[<p>I'm going to update the website front page to better showcase the application. I have most of the sections right, but the part I'm still not sure about is the top tagline, so I'm wondering if anyone had any suggestion about it?</p>
<p>From what I can see on Google Keep or Evernote for example it should be something like &quot;Use our app to get X or Y benefit&quot;, it should be a sentence that directly speaks to the user essentially.</p>
<p>So far I have &quot;Your notes, anywhere you are&quot; but I'm not certain that's particularly inspiring. Any other idea about what tagline could be used?</p>
]]></description><link>https://joplinapp.org/news/20210705-094247/</link><guid isPermaLink="false">20210705-094247</guid><pubDate>Mon, 05 Jul 2021 09:42:47 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20210705-094247/</link><guid isPermaLink="false">20210705-094247</guid><pubDate>Mon, 05 Jul 2021 09:42:47 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Poll: What's the size of your note collection?]]></title><description><![CDATA[<p>Poll is on the forum:</p>
<p><a href="https://discourse.joplinapp.org/t/poll-whats-the-size-of-your-note-collection/18191">https://discourse.joplinapp.org/t/poll-whats-the-size-of-your-note-collection/18191</a></p>
]]></description><link>https://joplinapp.org/news/20210624-171844/</link><guid isPermaLink="false">20210624-171844</guid><pubDate>Thu, 24 Jun 2021 17:18:44 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>

View File

@@ -1,7 +1,5 @@
<!-- Monthly/Yearly plan A/B testing -->
<!--
<script src="https://www.googleoptimize.com/optimize.js?id=OPT-PW3ZPK3"></script>
-->
<!-- Donate button A/B testing -->
<!--

View File

@@ -45,11 +45,11 @@ Building the apps is relatively easy - please [see the build instructions](https
## Coding style
Please see [readme/coding_style.md](readme/coding_style.md).
Coding style is enforced by a pre-commit hook that runs eslint. This hook is installed whenever running `yarn install` on any of the application directory. If for some reason the pre-commit hook didn't get installed, you can manually install it by running `yarn install` at the root of the repository.
## GUI style
For new React components, please use [React Hooks](https://reactjs.org/docs/hooks-intro.html). For new code in general, please use TypeScript. Even if you are modifying a file that was originally in JavaScript you should ideally convert it first to TypeScript before modifying it. Doing so is relatively easy and it helps maintain code quality.
For changes made to the Desktop and mobile clients that affect the user interface, refer to `packages/lib/theme.ts` for all styling information. The goal is to create a consistent user interface to allow for easy navigation of Joplin's various features and improve the overall user experience.
For changes made to the Desktop client that affect the user interface, refer to `packages/app-desktop/theme.ts` for all styling information. The goal is to create a consistent user interface to allow for easy navigation of Joplin's various features and improve the overall user experience.
## Automated tests

View File

@@ -4,10 +4,6 @@
* * *
Joplin will have [its first Meetup on 30 August 2022](https://discourse.joplinapp.org/t/joplin-first-meetup-on-30-august/26808)! Come and join us at the Old Thameside Inn next to London Bridge!
* * *
🌞 Joplin participates in **Google Summer of Code 2022**! More info on [the announcement post](https://github.com/laurent22/joplin/blob/dev/readme/news/20220308-gsoc2022-start.md). 🌞
* * *
@@ -87,9 +83,8 @@ A community maintained list of these distributions can be found here: [Unofficia
| <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/49439044?s=96&v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/77214738?s=96&v=4"/></br>[Polymathic-Company](https://github.com/Polymathic-Company) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/54626606?s=96&v=4"/></br>[skyrunner15](https://github.com/skyrunner15) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) |
| | | | |
| <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/77214738?s=96&v=4"/></br>[Polymathic-Company](https://github.com/Polymathic-Company) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | | |
<!-- SPONSORS-GITHUB -->
<!-- TOC -->

View File

@@ -325,7 +325,6 @@
"homenote",
"hotfolder",
"Howver",
"hpagent",
"Hrvatska",
"htmlentities",
"htmlfile",
@@ -951,4 +950,4 @@
"မြန်မာ",
"កម្ពុជា"
]
}
}

View File

@@ -412,7 +412,7 @@ class AppGui {
const widget = this.widget('mainWindow').focusedWidget;
if (!widget) return null;
if (widget.name === 'noteList' || widget.name === 'folderList') {
if (widget.name == 'noteList' || widget.name == 'folderList') {
return widget.currentItem;
}
@@ -521,11 +521,11 @@ class AppGui {
const args = splitCommandString(cmd);
for (let i = 0; i < args.length; i++) {
if (args[i] === '$n') {
if (args[i] == '$n') {
args[i] = note ? note.id : '';
} else if (args[i] === '$b') {
} else if (args[i] == '$b') {
args[i] = folder ? folder.id : '';
} else if (args[i] === '$c') {
} else if (args[i] == '$c') {
const item = this.activeListItem();
args[i] = item ? item.id : '';
}

View File

@@ -81,21 +81,21 @@ class Application extends BaseApplication {
pattern = pattern ? pattern.toString() : '';
if (type === BaseModel.TYPE_FOLDER && (pattern === Folder.conflictFolderTitle() || pattern === Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (type == BaseModel.TYPE_FOLDER && (pattern == Folder.conflictFolderTitle() || pattern == Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (!options) options = {};
const parent = options.parent ? options.parent : app().currentFolder();
const ItemClass = BaseItem.itemClass(type);
if (type === BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) {
if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) {
// Handle it as pattern
if (!parent) throw new Error(_('No notebook selected.'));
return await Note.previews(parent.id, { titlePattern: pattern });
} else {
// Single item
let item = null;
if (type === BaseModel.TYPE_NOTE) {
if (type == BaseModel.TYPE_NOTE) {
if (!parent) throw new Error(_('No notebook has been specified.'));
item = await ItemClass.loadFolderNoteByField(parent.id, 'title', pattern);
} else {
@@ -137,7 +137,7 @@ class Application extends BaseApplication {
if (!options.booleanAnswerDefault) options.booleanAnswerDefault = 'y';
if (!options.answers) options.answers = options.booleanAnswerDefault === 'y' ? [_('Y'), _('n')] : [_('N'), _('y')];
if (options.type === 'boolean') {
if (options.type == 'boolean') {
message += ` (${options.answers.join('/')})`;
}
@@ -146,7 +146,7 @@ class Application extends BaseApplication {
if (options.type === 'boolean') {
if (answer === null) return false; // Pressed ESCAPE
if (!answer) answer = options.answers[0];
const positiveIndex = options.booleanAnswerDefault === 'y' ? 0 : 1;
const positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
} else {
return answer;
@@ -181,7 +181,7 @@ class Application extends BaseApplication {
fs.readdirSync(__dirname).forEach(path => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path);
if (ext !== 'js') return;
if (ext != 'js') return;
const CommandClass = require(`./${path}`);
let cmd = new CommandClass();

View File

@@ -12,7 +12,7 @@ async function handleAutocompletionPromise(line) {
const words = getArguments(line);
// If there is only one word and it is not already a command name then you
// should look for commands it could be
if (words.length === 1) {
if (words.length == 1) {
if (names.indexOf(words[0]) === -1) {
const x = names.filter(n => n.indexOf(words[0]) === 0);
if (x.length === 1) {
@@ -78,38 +78,38 @@ async function handleAutocompletionPromise(line) {
const currentFolder = app().currentFolder();
if (argName === 'note' || argName === 'note-pattern') {
if (argName == 'note' || argName == 'note-pattern') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: `${next}*` }) : [];
l.push(...notes.map(n => n.title));
}
if (argName === 'notebook') {
if (argName == 'notebook') {
const folders = await Folder.search({ titlePattern: `${next}*` });
l.push(...folders.map(n => n.title));
}
if (argName === 'item') {
if (argName == 'item') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: `${next}*` }) : [];
const folders = await Folder.search({ titlePattern: `${next}*` });
l.push(...notes.map(n => n.title), folders.map(n => n.title));
}
if (argName === 'tag') {
if (argName == 'tag') {
const tags = await Tag.search({ titlePattern: `${next}*` });
l.push(...tags.map(n => n.title));
}
if (argName === 'file') {
if (argName == 'file') {
const files = await fs.readdir('.');
l.push(...files);
}
if (argName === 'tag-command') {
if (argName == 'tag-command') {
const c = filterList(['add', 'remove', 'list', 'notetags'], next);
l.push(...c);
}
if (argName === 'todo-command') {
if (argName == 'todo-command') {
const c = filterList(['toggle', 'clear'], next);
l.push(...c);
}

View File

@@ -52,7 +52,7 @@ function getCommands() {
fs.readdirSync(__dirname).forEach(path => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path);
if (ext !== 'js') return;
if (ext != 'js') return;
const CommandClass = require(`./${path}`);
const cmd = new CommandClass();

View File

@@ -222,7 +222,7 @@ async function main() {
for (const n in testUnits) {
if (!testUnits.hasOwnProperty(n)) continue;
if (onlyThisTest && n !== onlyThisTest) continue;
if (onlyThisTest && n != onlyThisTest) continue;
await clearDatabase();
const testName = n.substr(4).toLowerCase();

View File

@@ -21,7 +21,7 @@ cliUtils.printArray = function(logFunction, rows) {
for (let j = 0; j < row.length; j++) {
const item = row[j];
const width = item ? item.toString().length : 0;
const align = typeof item === 'number' ? ALIGN_RIGHT : ALIGN_LEFT;
const align = typeof item == 'number' ? ALIGN_RIGHT : ALIGN_LEFT;
if (!colWidths[j] || colWidths[j] < width) colWidths[j] = width;
if (colAligns.length <= j) colAligns[j] = align;
}
@@ -32,7 +32,7 @@ cliUtils.printArray = function(logFunction, rows) {
for (let col = 0; col < colWidths.length; col++) {
const item = rows[row][col];
const width = colWidths[col];
const dir = colAligns[col] === ALIGN_LEFT ? stringPadding.RIGHT : stringPadding.LEFT;
const dir = colAligns[col] == ALIGN_LEFT ? stringPadding.RIGHT : stringPadding.LEFT;
line.push(stringPadding(item, width, ' ', dir));
}
logFunction(line.join(' '));
@@ -45,13 +45,13 @@ cliUtils.parseFlags = function(flags) {
for (let i = 0; i < flags.length; i++) {
let f = flags[i].trim();
if (f.substr(0, 2) === '--') {
if (f.substr(0, 2) == '--') {
f = f.split(' ');
output.long = f[0].substr(2).trim();
if (f.length === 2) {
if (f.length == 2) {
output.arg = cliUtils.parseCommandArg(f[1].trim());
}
} else if (f.substr(0, 1) === '-') {
} else if (f.substr(0, 1) == '-') {
output.short = f.substr(1);
}
}
@@ -65,9 +65,9 @@ cliUtils.parseCommandArg = function(arg) {
const c2 = arg[arg.length - 1];
const name = arg.substr(1, arg.length - 2);
if (c1 === '<' && c2 === '>') {
if (c1 == '<' && c2 == '>') {
return { required: true, name: name };
} else if (c1 === '[' && c2 === ']') {
} else if (c1 == '[' && c2 == ']') {
return { required: false, name: name };
} else {
throw new Error(`Invalid command arg: ${arg}`);
@@ -83,7 +83,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
const booleanFlags = [];
const aliases = {};
for (let i = 0; i < options.length; i++) {
if (options[i].length !== 2) throw new Error(`Invalid options: ${options[i]}`);
if (options[i].length != 2) throw new Error(`Invalid options: ${options[i]}`);
let flags = options[i][0];
flags = cliUtils.parseFlags(flags);
@@ -117,7 +117,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
const argOptions = {};
for (const key in args) {
if (!args.hasOwnProperty(key)) continue;
if (key === '_') continue;
if (key == '_') continue;
argOptions[key] = args[key];
}
@@ -170,7 +170,7 @@ cliUtils.promptConfirm = function(message, answers = null) {
return new Promise((resolve) => {
rl.question(`${message} `, answer => {
const ok = !answer || answer.toLowerCase() === answers[0].toLowerCase();
const ok = !answer || answer.toLowerCase() == answers[0].toLowerCase();
rl.close();
resolve(ok);
});

View File

@@ -122,7 +122,7 @@ class Command extends BaseCommand {
}
if (args.name === 'locale') {
if (args.name == 'locale') {
setLocale(Setting.value('locale'));
}

View File

@@ -44,7 +44,7 @@ class Command extends BaseCommand {
queryOptions.orderBy = options.sort;
queryOptions.orderByDir = 'ASC';
}
if (options.reverse === true) queryOptions.orderByDir = queryOptions.orderByDir === 'ASC' ? 'DESC' : 'ASC';
if (options.reverse === true) queryOptions.orderByDir = queryOptions.orderByDir == 'ASC' ? 'DESC' : 'ASC';
queryOptions.caseInsensitive = true;
if (options.type) {
queryOptions.itemTypes = [];
@@ -55,7 +55,7 @@ class Command extends BaseCommand {
queryOptions.uncompletedTodosOnTop = Setting.value('uncompletedTodosOnTop');
let modelType = null;
if (pattern === '/' || !app().currentFolder()) {
if (pattern == '/' || !app().currentFolder()) {
queryOptions.includeConflictFolder = true;
items = await Folder.all(queryOptions);
modelType = Folder.modelType();
@@ -65,7 +65,7 @@ class Command extends BaseCommand {
modelType = Note.modelType();
}
if (options.format && options.format === 'json') {
if (options.format && options.format == 'json') {
this.stdout(JSON.stringify(items));
} else {
let hasTodos = false;
@@ -88,7 +88,7 @@ class Command extends BaseCommand {
row.push(BaseModel.shortId(item.id));
shortIdShown = true;
if (modelType === Folder.modelType()) {
if (modelType == Folder.modelType()) {
row.push(await Folder.noteCount(item.id));
}

View File

@@ -133,7 +133,7 @@ class Command extends BaseCommand {
this.releaseLockFn_ = await Command.lockFile(lockFilePath);
} catch (error) {
if (error.code === 'ELOCKED') {
if (error.code == 'ELOCKED') {
const msg = _('Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at "%s" and resume the operation.', error.file);
this.stdout(msg);
return;
@@ -222,7 +222,7 @@ class Command extends BaseCommand {
const newContext = await sync.start(options);
Setting.setValue(contextKey, JSON.stringify(newContext));
} catch (error) {
if (error.code === 'alreadyStarted') {
if (error.code == 'alreadyStarted') {
this.stdout(error.message);
} else {
throw error;

View File

@@ -30,21 +30,21 @@ class Command extends BaseCommand {
const command = args['tag-command'];
if (command === 'remove' && !tag) throw new Error(_('Cannot find "%s".', args.tag));
if (command == 'remove' && !tag) throw new Error(_('Cannot find "%s".', args.tag));
if (command === 'add') {
if (command == 'add') {
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
if (!tag) tag = await Tag.save({ title: args.tag }, { userSideValidation: true });
for (let i = 0; i < notes.length; i++) {
await Tag.addNote(tag.id, notes[i].id);
}
} else if (command === 'remove') {
} else if (command == 'remove') {
if (!tag) throw new Error(_('Cannot find "%s".', args.tag));
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
for (let i = 0; i < notes.length; i++) {
await Tag.removeNote(tag.id, notes[i].id);
}
} else if (command === 'list') {
} else if (command == 'list') {
if (tag) {
const notes = await Tag.notes(tag.id);
notes.map(note => {
@@ -75,7 +75,7 @@ class Command extends BaseCommand {
this.stdout(tag.title);
});
}
} else if (command === 'notetags') {
} else if (command == 'notetags') {
if (args.tag) {
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.tag);
if (!note) throw new Error(_('Cannot find "%s".', args.tag));

View File

@@ -29,13 +29,13 @@ class Command extends BaseCommand {
id: note.id,
};
if (action === 'toggle') {
if (action == 'toggle') {
if (!note.is_todo) {
toSave = Note.toggleIsTodo(note);
} else {
toSave.todo_completed = note.todo_completed ? 0 : time.unixMs();
}
} else if (action === 'clear') {
} else if (action == 'clear') {
toSave.is_todo = 0;
}

View File

@@ -2071,7 +2071,7 @@ function execCommand(client, command, options = {}) {
return new Promise((resolve, reject) => {
const childProcess = exec(cmd, (error, stdout, stderr) => {
if (error) {
if (error.signal === 'SIGTERM') {
if (error.signal == 'SIGTERM') {
resolve('Process was killed');
} else {
logger.error(stderr);
@@ -2103,7 +2103,7 @@ async function clientItems(client) {
function randomTag(items) {
const tags = [];
for (let i = 0; i < items.length; i++) {
if (items[i].type_ !== 5) continue;
if (items[i].type_ != 5) continue;
tags.push(items[i]);
}
@@ -2113,7 +2113,7 @@ function randomTag(items) {
function randomNote(items) {
const notes = [];
for (let i = 0; i < items.length; i++) {
if (items[i].type_ !== 1) continue;
if (items[i].type_ != 1) continue;
notes.push(items[i]);
}
@@ -2131,11 +2131,11 @@ async function execRandomCommand(client) {
const item = randomElement(items);
if (!item) return;
if (item.type_ === 1) {
if (item.type_ == 1) {
return execCommand(client, `rm -f ${item.id}`);
} else if (item.type_ === 2) {
} else if (item.type_ == 2) {
return execCommand(client, `rm -r -f ${item.id}`);
} else if (item.type_ === 5) {
} else if (item.type_ == 5) {
// tag
} else {
throw new Error(`Unknown type: ${item.type_}`);
@@ -2213,7 +2213,7 @@ function randomNextCheckTime() {
function findItem(items, itemId) {
for (let i = 0; i < items.length; i++) {
if (items[i].id === itemId) return items[i];
if (items[i].id == itemId) return items[i];
}
return null;
}
@@ -2225,7 +2225,7 @@ function compareItems(item1, item2) {
const p1 = item1[n];
const p2 = item2[n];
if (n === 'notes_') {
if (n == 'notes_') {
p1.sort();
p2.sort();
if (JSON.stringify(p1) !== JSON.stringify(p2)) {
@@ -2246,7 +2246,7 @@ function findMissingItems_(items1, items2) {
let found = false;
for (let j = 0; j < items2.length; j++) {
const item2 = items2[j];
if (item1.id === item2.id) {
if (item1.id == item2.id) {
found = true;
break;
}
@@ -2340,9 +2340,9 @@ async function main() {
let state = 'commands';
setInterval(async () => {
if (state === 'waitForSyncCheck') return;
if (state == 'waitForSyncCheck') return;
if (state === 'syncCheck') {
if (state == 'syncCheck') {
state = 'waitForSyncCheck';
const clientItems = [];
// Up to 3 sync operations must be performed by each clients in order for them
@@ -2371,7 +2371,7 @@ async function main() {
return;
}
if (state === 'waitForClients') {
if (state == 'waitForClients') {
for (let i = 0; i < clients.length; i++) {
if (clients[i].activeCommandCount > 0) return;
}
@@ -2380,7 +2380,7 @@ async function main() {
return;
}
if (state === 'commands') {
if (state == 'commands') {
if (nextSyncCheckTime <= time.unixMs()) {
state = 'waitForClients';
return;

View File

@@ -2,7 +2,6 @@ const Folder = require('@joplin/lib/models/Folder').default;
const Tag = require('@joplin/lib/models/Tag').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
const ListWidget = require('tkwidgets/ListWidget.js');
const Setting = require('@joplin/lib/models/Setting').default;
const _ = require('@joplin/lib/locale')._;
class FolderListWidget extends ListWidget {
@@ -26,18 +25,6 @@ class FolderListWidget extends ListWidget {
output.push('-'.repeat(this.innerWidth));
} else if (item.type_ === Folder.modelType()) {
output.push(' '.repeat(this.folderDepth(this.folders, item.id)) + Folder.displayTitle(item));
if (Setting.value('showNoteCounts')) {
let noteCount = item.note_count;
// Subtract children note_count from parent folder.
if (this.folderHasChildren_(this.folders,item.id)) {
for (let i = 0; i < this.folders.length; i++) {
if (this.folders[i].parent_id === item.id) {
noteCount -= this.folders[i].note_count;
}
}
}
output.push(noteCount);
}
} else if (item.type_ === Tag.modelType()) {
output.push(`[${Folder.displayTitle(item)}]`);
} else if (item.type_ === BaseModel.TYPE_SEARCH) {

View File

@@ -26,7 +26,7 @@ const sharp = require('sharp');
const { shimInit } = require('@joplin/lib/shim-init-node.js');
const shim = require('@joplin/lib/shim').default;
const { _ } = require('@joplin/lib/locale');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const envFromArgs = require('@joplin/lib/envFromArgs');
const nodeSqlite = require('sqlite3');
@@ -82,13 +82,13 @@ if (process.platform === 'win32') {
process.stdout.on('error', function(err) {
// https://stackoverflow.com/questions/12329816/error-write-epipe-when-piping-node-output-to-head#15884508
if (err.code === 'EPIPE') {
if (err.code == 'EPIPE') {
process.exit(0);
}
});
application.start(process.argv).catch(error => {
if (error.code === 'flagError') {
if (error.code == 'flagError') {
console.error(error.message);
console.error(_('Type `joplin help` for usage information.'));
} else {

View File

@@ -33,14 +33,14 @@
],
"owner": "Laurent Cozic"
},
"version": "2.9.0",
"version": "2.8.1",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~2.9",
"@joplin/renderer": "~2.9",
"@joplin/lib": "~2.8",
"@joplin/renderer": "~2.8",
"aws-sdk": "^2.588.0",
"chalk": "^4.1.0",
"compare-version": "^0.1.2",
@@ -67,7 +67,7 @@
"yargs-parser": "^7.0.0"
},
"devDependencies": {
"@joplin/tools": "~2.9",
"@joplin/tools": "~2.8",
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",

View File

@@ -30,7 +30,7 @@ describe('feature_NoteHistory', function() {
});
afterEach(async (done) => {
if (testApp) await testApp.destroy();
if (testApp !== null) await testApp.destroy();
testApp = null;
done();
});

View File

@@ -32,15 +32,6 @@
}
}
function escapeHtml(s) {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function pageTitle() {
const titleElements = document.getElementsByTagName('title');
if (titleElements.length) return titleElements[0].text.trim();
@@ -213,16 +204,6 @@
}
}
if (nodeName === 'embed') {
const src = absoluteUrl(node.src);
node.setAttribute('src', src);
}
if (nodeName === 'object') {
const data = absoluteUrl(node.data);
node.setAttribute('data', data);
}
cleanUpElement(convertToMarkup, node, imageSizes, imageIndexes);
}
}
@@ -336,9 +317,6 @@
}
function readabilityProcess() {
if (isPagePdf()) throw new Error('Could not parse PDF document with Readability');
// eslint-disable-next-line no-undef
const readability = new Readability(documentForReadability());
const article = readability.parse();
@@ -351,14 +329,6 @@
};
}
function isPagePdf() {
return document.contentType === 'application/pdf';
}
function embedPageUrl() {
return `<embed src="${escapeHtml(window.location.href)}" type="${escapeHtml(document.contentType)}" />`;
}
async function prepareCommandResponse(command) {
console.info(`Got command: ${command.name}`);
const shouldSendToJoplin = !!command.shouldSendToJoplin;
@@ -405,10 +375,6 @@
} else if (command.name === 'completePageHtml') {
if (isPagePdf()) {
return clippedContentResponse(pageTitle(), embedPageUrl(), getImageSizes(document), getAnchorNames(document));
}
hardcodePreStyles(document);
addSvgClass(document);
preProcessDocument(document);

View File

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

View File

@@ -16,8 +16,6 @@ function getAdditionalModulePaths(options = {}) {
// We need to explicitly check for null and undefined (and not a falsy value) because
// TypeScript treats an empty string as `.`.
//
// eslint-disable-next-line eqeqeq
if (baseUrl == null) {
// If there's no baseUrl set we respect NODE_PATH
// Note that NODE_PATH is deprecated and will be removed

View File

@@ -20253,19 +20253,6 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"node_modules/typescript": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -38008,12 +37995,6 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"peer": true
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",

View File

@@ -67,7 +67,7 @@ checkBrowsers(paths.appPath, isInteractive)
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
if (!port) {
if (port == null) {
// We have not found a port.
return;
}

View File

@@ -104,22 +104,22 @@ class Application extends BaseApplication {
}
protected async generalMiddleware(store: any, next: any, action: any) {
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'locale' || action.type === 'SETTING_UPDATE_ALL') {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
setLocale(Setting.value('locale'));
// The bridge runs within the main process, with its own instance of locale.js
// so it needs to be set too here.
bridge().setLocale(Setting.value('locale'));
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'showTrayIcon' || action.type === 'SETTING_UPDATE_ALL') {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
this.updateTray();
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'style.editor.fontFamily' || action.type === 'SETTING_UPDATE_ALL') {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'style.editor.fontFamily' || action.type == 'SETTING_UPDATE_ALL') {
this.updateEditorFont();
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'windowContentZoomFactor' || action.type === 'SETTING_UPDATE_ALL') {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'windowContentZoomFactor' || action.type == 'SETTING_UPDATE_ALL') {
webFrame.setZoomFactor(Setting.value('windowContentZoomFactor') / 100);
}
@@ -142,7 +142,7 @@ class Application extends BaseApplication {
await Folder.expandTree(newState.folders, action.folderId);
}
if (this.hasGui() && ((action.type === 'SETTING_UPDATE_ONE' && ['themeAutoDetect', 'theme', 'preferredLightTheme', 'preferredDarkTheme'].includes(action.key)) || action.type === 'SETTING_UPDATE_ALL')) {
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && ['themeAutoDetect', 'theme', 'preferredLightTheme', 'preferredDarkTheme'].includes(action.key)) || action.type == 'SETTING_UPDATE_ALL')) {
this.handleThemeAutoDetect();
}
@@ -387,7 +387,7 @@ class Application extends BaseApplication {
PerFolderSortOrderService.initialize();
CommandService.instance().initialize(this.store(), Setting.value('env') === 'dev', stateToWhenClauseContext);
CommandService.instance().initialize(this.store(), Setting.value('env') == 'dev', stateToWhenClauseContext);
for (const command of commands) {
CommandService.instance().registerDeclaration(command.declaration);

View File

@@ -86,7 +86,7 @@ async function fetchLatestRelease(options: CheckForUpdateOptions) {
const ext = fileExtension(asset.name);
if (platform === 'win32' && ext === 'exe') {
if (shim.isPortable()) {
found = asset.name === 'JoplinPortable.exe';
found = asset.name == 'JoplinPortable.exe';
} else {
found = !!asset.name.match(/^Joplin-Setup-[\d.]+\.exe$/);
}

View File

@@ -4,7 +4,7 @@ const os = require('os');
const sha512 = require('js-sha512');
const generateChecksumFile = () => {
if (os.platform() !== 'linux') {
if (os.platform() != 'linux') {
return []; // SHA-512 is only for AppImage
}
const distDirName = 'dist';
@@ -18,7 +18,7 @@ const generateChecksumFile = () => {
break;
}
}
if (appImageName === '') {
if (appImageName == '') {
throw 'AppImage not found!';
}
const appImagePath = path.join(distPath, appImageName);

View File

@@ -174,7 +174,7 @@ function useMenuStates(menu: any, props: Props) {
menuItemSetChecked(`sort:${type}:${field}`, (props as any)[`${type}.sortOrder.field`] === field);
}
const id = type === 'notes' ? 'toggleNotesSortOrderReverse' : `sort:${type}:reverse`;
const id = type == 'notes' ? 'toggleNotesSortOrderReverse' : `sort:${type}:reverse`;
menuItemSetChecked(id, (props as any)[`${type}.sortOrder.reverse`]);
}
@@ -332,7 +332,7 @@ function useMenu(props: Props) {
sortItems.push({ type: 'separator' });
if (type === 'notes') {
if (type == 'notes') {
sortItems.push(
{ ...menuItemDic.toggleNotesSortOrderReverse, type: 'checkbox' },
{ ...menuItemDic.toggleNotesSortOrderField, visible: false }

View File

@@ -38,6 +38,7 @@ import ErrorBoundary from '../../../ErrorBoundary';
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
import eventManager from '@joplin/lib/eventManager';
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
import { checkTableIsUnderCursor, readTableAroundCursor } from './utils/tables';
const menuUtils = new MenuUtils(CommandService.instance());
@@ -614,8 +615,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
resourceInfos: props.resourceInfos,
contentMaxWidth: props.contentMaxWidth,
mapsToLine: true,
// Always using useCustomPdfViewer for now, we can add a new setting for it in future if we need to.
useCustomPdfViewer: true,
}));
if (cancelled) return;
@@ -708,8 +707,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return output;
}, [styles.cellViewer, props.visiblePanes]);
const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
useEffect(() => {
if (!editorRef.current) return;
@@ -717,10 +714,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
// we should focus the editor
// The intuition is that a panel toggle (with editor in view) is the equivalent of
// an editor interaction so users should expect the editor to be focused
if (editorPaneVisible) {
if (props.visiblePanes.indexOf('editor') >= 0) {
editorRef.current.focus();
}
}, [editorPaneVisible]);
}, [props.visiblePanes]);
useEffect(() => {
if (!editorRef.current) return;
@@ -757,7 +754,14 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
const cm = editorRef.current;
const hasSelectedText = cm && !!cm.getSelection() ;
const tableIsUnderCursor = checkTableIsUnderCursor(cm);
let tableUnderCursor: string = null;
if (tableIsUnderCursor) tableUnderCursor = readTableAroundCursor(cm);
menu.append(
new MenuItem({
@@ -789,6 +793,27 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
})
);
if (tableUnderCursor) {
menu.append(
new MenuItem({ type: 'separator' })
);
menu.append(
new MenuItem({
label: _('Edit table...'),
click: async () => {
props.dispatch({
type: 'DIALOG_OPEN',
name: 'tableEditor',
props: {
markdownTable: tableUnderCursor,
},
});
},
})
);
}
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {

View File

@@ -0,0 +1,48 @@
function findElementWithClass(element: any, className: string): any {
if (element.classList && element.classList.contains(className)) return element;
for (const child of element.childNodes) {
const hasClass = findElementWithClass(child, className);
if (hasClass) return hasClass;
}
return null;
}
export const checkTableIsUnderCursor = (cm: any) => {
if (!cm) return false;
const coords = cm.cursorCoords(cm.getCursor());
const element = document.elementFromPoint(coords.left, coords.top);
if (!element) return false;
return !!findElementWithClass(element, 'cm-jn-table-item');
};
export const readTableAroundCursor = (cm: any) => {
const idxAtCursor = cm.doc.getCursor().line;
const lineCount = cm.lineCount();
const lines: string[] = [];
for (let i = idxAtCursor - 1; i >= 0; i--) {
const line: string = cm.doc.getLine(i);
if (line.startsWith('|')) {
lines.splice(0, 0, line);
} else {
break;
}
}
lines.push(cm.doc.getLine(idxAtCursor));
for (let i = idxAtCursor + 1; i < lineCount; i++) {
const line: string = cm.doc.getLine(i);
if (line.startsWith('|')) {
lines.push(line);
} else {
break;
}
}
return lines.join('\n');
};

View File

@@ -37,7 +37,7 @@ export default function useEditorSearch(CodeMirror: any) {
return { token: function(stream: any) {
query.lastIndex = stream.pos;
const match = query.exec(stream.string);
if (match && match.index === stream.pos) {
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1;
return 'search-marker';
} else if (match) {
@@ -126,7 +126,7 @@ export default function useEditorSearch(CodeMirror: any) {
// SEARCHOVERLAY
// We only want to highlight all matches when there is only 1 search term
if (keywords.length !== 1 || keywords[0].value === '') {
if (keywords.length !== 1 || keywords[0].value == '') {
clearOverlay(this);
const prev = keywords.length > 1 ? keywords[0].value : '';
setPreviousKeywordValue(prev);

View File

@@ -165,6 +165,7 @@ export default function useJoplinMode(CodeMirror: any) {
}
if (isMonospace) { token = `${token} jn-monospace`; }
if (state.inTable) { token = `${token} jn-table-item`; }
// //////// End Monospace //////////
return token;

View File

@@ -2078,17 +2078,6 @@
setup(editor);
};
var setup$2 = function (editor) {
var editorClickHandler = function (event) {
if (!isJoplinChecklistItem(event.target))
return;
if (event.offsetX >= 0)
return;
editor.execCommand('ToggleJoplinChecklistItem', false, { element: event.target });
};
editor.on('click', editorClickHandler);
};
var findIndex = function (list, predicate) {
for (var index = 0; index < list.length; index++) {
var element = list[index];
@@ -2111,8 +2100,21 @@
var listType = findContainerListTypeFromEvent(e);
buttonApi.setActive(listType === options.listType && lists.length > 0 && lists[0].nodeName === listName && !isCustomList(lists[0]));
};
var editorClickHandler = function (event) {
if (!isJoplinChecklistItem(event.target))
return;
if (event.offsetX >= 0)
return;
editor.execCommand('ToggleJoplinChecklistItem', false, { element: event.target });
};
if (options.listType === 'joplinChecklist') {
editor.on('click', editorClickHandler);
}
editor.on('NodeChange', nodeChangeHandler);
return function () {
if (options.listType === 'joplinChecklist') {
editor.off('click', editorClickHandler);
}
editor.off('NodeChange', nodeChangeHandler);
};
};
@@ -2156,7 +2158,6 @@
function Plugin () {
PluginManager.add('joplinLists', function (editor) {
setup$1(editor);
setup$2(editor);
register$1(editor);
register(editor);
return get(editor);

View File

@@ -100,7 +100,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
label: _('Save as %s', 'PNG'),
onAction: async (options: ContextMenuOptions) => {
// First convert it to png then save
if (options.mime !== 'image/svg+xml') {
if (options.mime != 'image/svg+xml') {
throw new Error(`Unsupported image type: ${options.mime}`);
}
if (!options.filename) {
@@ -151,14 +151,14 @@ export function menuItems(dispatch: Function): ContextMenuItems {
handleCopyToClipboard(options);
options.insertContent('');
},
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType !== ContextMenuItemType.Image && (!options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy)),
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy)),
},
copy: {
label: _('Copy'),
onAction: async (options: ContextMenuOptions) => {
handleCopyToClipboard(options);
},
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType !== ContextMenuItemType.Image && (!!options.textToCopy || !!options.htmlToCopy),
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!!options.textToCopy || !!options.htmlToCopy),
},
paste: {
label: _('Paste'),

View File

@@ -114,7 +114,7 @@ export default function useFormNote(dependencies: HookDependencies) {
if (syncStarted) return () => {};
if (formNote.hasChanged) return () => {};
reg.logger().info('Sync has finished and note has never been changed - reloading it');
reg.logger().debug('Sync has finished and note has never been changed - reloading it');
let cancelled = false;

View File

@@ -20,7 +20,6 @@ export interface MarkupToHtmlOptions {
plugins?: Record<string, any>;
bodyOnly?: boolean;
mapsToLine?: boolean;
useCustomPdfViewer?: boolean;
}
export default function useMarkupToHtml(deps: HookDependencies) {

View File

@@ -342,7 +342,7 @@ const NoteListComponent = (props: Props) => {
const keyCode = event.keyCode;
const noteIds = props.selectedNoteIds;
if (noteIds.length > 0 && (keyCode === 40 || keyCode === 38 || keyCode === 33 || keyCode === 34 || keyCode === 35 || keyCode === 36)) {
if (noteIds.length > 0 && (keyCode === 40 || keyCode === 38 || keyCode === 33 || keyCode === 34 || keyCode === 35 || keyCode == 36)) {
// DOWN / UP / PAGEDOWN / PAGEUP / END / HOME
const noteId = noteIds[0];
let noteIndex = BaseModel.modelIndexById(props.notes, noteId);
@@ -456,11 +456,6 @@ const NoteListComponent = (props: Props) => {
useEffect(() => {
// When a note list item is styled by userchrome.css, its height is reflected.
// Ref. https://github.com/laurent22/joplin/pull/6542
if (dragOverTargetNoteIndex !== null) {
// When dragged, its height should not be considered.
// Ref. https://github.com/laurent22/joplin/issues/6639
return;
}
const noteItem = Object.values<any>(itemAnchorRefs_.current)[0]?.current;
const actualItemHeight = noteItem?.getHeight() ?? 0;
if (actualItemHeight >= 8) { // To avoid generating too many narrow items

View File

@@ -41,7 +41,7 @@ class NotePropertiesDialog extends React.Component {
}
componentDidUpdate() {
if (this.state.editedKey === null) {
if (this.state.editedKey == null) {
this.okButton.current.focus();
}
}
@@ -59,7 +59,7 @@ class NotePropertiesDialog extends React.Component {
latLongFromLocation(location) {
const o = {};
const l = location.split(',');
if (l.length === 2) {
if (l.length == 2) {
o.latitude = l[0].trim();
o.longitude = l[1].trim();
} else {

View File

@@ -22,6 +22,7 @@ import Dialog from './Dialog';
import SyncWizardDialog from './SyncWizard/Dialog';
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
import EditFolderDialog from './EditFolderDialog/Dialog';
import TableEditorDialog from './TableEditorDialog/Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
const { ImportScreen } = require('./ImportScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
@@ -38,6 +39,7 @@ interface Props {
zoomFactor: number;
needApiAuth: boolean;
dialogs: AppStateDialog[];
dialogContentMaxSize: Size;
}
interface ModalDialogProps {
@@ -51,6 +53,7 @@ interface RegisteredDialogProps {
themeId: number;
key: string;
dispatch: Function;
dialogContentMaxSize: Size;
}
interface RegisteredDialog {
@@ -60,19 +63,25 @@ interface RegisteredDialog {
const registeredDialogs: Record<string, RegisteredDialog> = {
syncWizard: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
masterPassword: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
editFolder: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
return <EditFolderDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
tableEditor: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <TableEditorDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
};
@@ -121,7 +130,7 @@ async function initialize() {
class RootComponent extends React.Component<Props, any> {
public async componentDidMount() {
if (this.props.appState === 'starting') {
if (this.props.appState == 'starting') {
this.props.dispatch({
type: 'APP_STATE_SET',
state: 'initializing',
@@ -195,10 +204,12 @@ class RootComponent extends React.Component<Props, any> {
for (const dialog of props.dialogs) {
const md = registeredDialogs[dialog.name];
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
output.push(md.render({
key: dialog.name,
themeId: props.themeId,
dispatch: props.dispatch,
dialogContentMaxSize: props.dialogContentMaxSize,
}, dialog.props));
}
return output;
@@ -245,6 +256,11 @@ const mapStateToProps = (state: AppState) => {
themeId: state.settings.theme,
needApiAuth: state.needApiAuth,
dialogs: state.dialogs,
dialogContentMaxSize: {
// Minus padding, margins and dialog header and button bar.
width: state.windowContentSize.width - 36 * 2,
height: state.windowContentSize.height - 36 * 2 - 28 - 30 - 20,
},
profileConfigCurrentProfileId: state.profileConfig.currentProfileId,
};
};

View File

@@ -119,7 +119,9 @@ function SearchBar(props: Props) {
}, [onExitSearch]);
const onSearchButtonClick = useCallback(() => {
if (props.isFocused || searchStarted) {
console.info('isFocused', props.isFocused);
if (props.isFocused) {
void onExitSearch();
} else {
setSearchStarted(true);
@@ -129,7 +131,7 @@ function SearchBar(props: Props) {
field: 'globalSearch',
});
}
}, [onExitSearch, props.isFocused, searchStarted]);
}, [onExitSearch, props.isFocused]);
useEffect(() => {
if (props.notesParentType !== 'Search') {

View File

@@ -0,0 +1,101 @@
import * as React from 'react';
import { useCallback, useEffect } from 'react';
import { _ } from '@joplin/lib/locale';
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
import Dialog from '../Dialog';
import DialogTitle from '../DialogTitle';
import { parseMarkdownTable } from '../../../lib/markdownUtils';
import { Size } from '../ResizableLayout/utils/types';
interface Props {
themeId: number;
dispatch: Function;
markdownTable: string;
dialogContentMaxSize: Size;
}
const markdownTableToObject = (markdownTable: string): any => {
const table = parseMarkdownTable(markdownTable);
return {
columns: table.headers.map(h => {
return {
title: h.label,
field: h.name,
hozAlign: h.justify,
editor: 'input',
};
}),
data: table.rows.map(row => {
return {
...row,
};
}),
};
};
export default function(props: Props) {
const elementId = `tabulator_${Math.floor(Math.random() * 1000000)}`;
const onClose = useCallback(() => {
props.dispatch({
type: 'DIALOG_CLOSE',
name: 'tableEditor',
});
}, [props.dispatch]);
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
if (event.buttonName === 'cancel') {
onClose();
return;
}
if (event.buttonName === 'ok') {
return;
}
}, [onClose]);
useEffect(() => {
const table = markdownTableToObject(props.markdownTable);
const Tabulator = (window as any).Tabulator;
// TODO: probably doesn't need to be called every time
// TODO: Load CSS/JS dynamically?
// TODO: Clean up on exit
Tabulator.extendModule('edit', 'editors', {});
new Tabulator(`#${elementId}`, {
...table,
height: props.dialogContentMaxSize.height,
});
}, []);
function renderContent() {
return (
<div className="dialog-content">
<div id={elementId}></div>
</div>
);
}
function renderDialogWrapper() {
return (
<div className="dialog-root">
<DialogTitle title={_('Edit table')}/>
{renderContent()}
<DialogButtonRow
themeId={props.themeId}
onClick={onButtonRowClick}
okButtonLabel={_('Save')}
/>
</div>
);
}
return (
<Dialog onClose={onClose} renderContent={renderDialogWrapper}/>
);
}

View File

@@ -653,17 +653,6 @@
e.preventDefault();
}));
document.addEventListener('click', webviewLib.logEnabledEventHandler(e => {
document.querySelectorAll('.media-pdf').forEach(element => {
if(!!element.contentWindow){
element.contentWindow.postMessage({
type: 'blur'
}, '*');
}
}
);
}));
let lastClientWidth_ = NaN, lastClientHeight_ = NaN, lastScrollTop_ = NaN;
window.addEventListener('resize', webviewLib.logEnabledEventHandler(() => {

View File

@@ -40,7 +40,7 @@ scrollmap.get_ = () => {
// embedded into elements by the renderer.
// See also renderer/MdToHtml/rules/source_map.ts.
const elems = document.getElementsByClassName('maps-to-line');
if (elems.length === 0) return null;
if (elems.length == 0) return null;
const map = { line: [0], percent: [0], viewHeight: height, lineCount: 0 };
// Each map entry is total-ordered.
let last = 0;

View File

@@ -7,6 +7,10 @@
uses 'eval'.
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
-->
<!--
To add files below, then need to be in the "vendor" directory. To make this happen, use copyApplicationAssets.js
-->
<title>Joplin</title>
<link rel="stylesheet" href="style.min.css">
<link rel="stylesheet" href="style/icons/style.css">
@@ -15,6 +19,9 @@
<link rel="stylesheet" href="vendor/lib/smalltalk/css/smalltalk.css">
<link rel="stylesheet" href="vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css">
<link rel="stylesheet" href="vendor/lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="vendor/lib/tabulator-tables/dist/css/tabulator.min.css">
<script type="text/javascript" src="vendor/lib/tabulator-tables/dist/js/tabulator.min.js"></script>
<style>
.smalltalk {

View File

@@ -26,7 +26,7 @@ const shim = require('@joplin/lib/shim').default;
const { shimInit } = require('@joplin/lib/shim-init-node.js');
const bridge = require('@electron/remote').require('./bridge').default;
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const React = require('react');
const nodeSqlite = require('sqlite3');
@@ -131,7 +131,7 @@ app().start(bridge().processArgv()).then((result) => {
}).catch((error) => {
const env = bridge().env();
if (error.code === 'flagError') {
if (error.code == 'flagError') {
bridge().showErrorMessageBox(error.message);
} else {
// If something goes wrong at this stage we don't have a console or a log file

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.9.4",
"version": "2.8.8",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -105,7 +105,7 @@
},
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"@joplin/tools": "~2.9",
"@joplin/tools": "~2.8",
"@testing-library/react-hooks": "^3.4.2",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
@@ -137,9 +137,8 @@
"@electron/remote": "^2.0.1",
"@fortawesome/fontawesome-free": "^5.13.0",
"@joeattardi/emoji-button": "^4.6.0",
"@joplin/lib": "~2.9",
"@joplin/pdf-viewer": "~2.9",
"@joplin/renderer": "~2.9",
"@joplin/lib": "~2.8",
"@joplin/renderer": "~2.8",
"async-mutex": "^0.1.3",
"codemirror": "^5.56.0",
"color": "^3.1.2",
@@ -175,6 +174,7 @@
"styled-components": "5.1.1",
"styled-system": "5.1.5",
"taboverride": "^4.0.3",
"tabulator-tables": "^5.1.4",
"tinymce": "^5.2.0"
}
}

View File

@@ -214,7 +214,7 @@ class Dialog extends React.PureComponent<Props, State> {
}
modalLayer_onClick(event: any) {
if (event.currentTarget === event.target) {
if (event.currentTarget == event.target) {
this.props.dispatch({
pluginName: PLUGIN_NAME,
type: 'PLUGINLEGACY_DIALOG_SET',

View File

@@ -1,12 +1,19 @@
import bridge from '../bridge';
import { Implementation as WindowImplementation } from '@joplin/lib/services/plugins/api/JoplinWindow';
import { injectCustomStyles } from '@joplin/lib/CssUtils';
import { VersionInfo } from '@joplin/lib/services/plugins/api/types';
import Setting from '@joplin/lib/models/Setting';
import { reg } from '@joplin/lib/registry';
import BasePlatformImplementation, { Joplin } from '@joplin/lib/services/plugins/BasePlatformImplementation';
const { clipboard, nativeImage } = require('electron');
const packageInfo = require('../../packageInfo');
interface JoplinViewsDialogs {
showMessageBox(message: string): Promise<number>;
}
interface JoplinViews {
dialogs: JoplinViewsDialogs;
}
interface Joplin {
views: JoplinViews;
}
interface Components {
[key: string]: any;
@@ -15,7 +22,7 @@ interface Components {
// PlatformImplementation provides access to platform specific dependencies,
// such as the clipboard, message dialog, etc. It allows having the same plugin
// API for all platforms, but with different implementations.
export default class PlatformImplementation extends BasePlatformImplementation {
export default class PlatformImplementation {
private static instance_: PlatformImplementation;
private joplin_: Joplin;
@@ -26,14 +33,6 @@ export default class PlatformImplementation extends BasePlatformImplementation {
return this.instance_;
}
public get versionInfo(): VersionInfo {
return {
version: packageInfo.version,
syncVersion: Setting.value('syncVersion'),
profileVersion: reg.db().version(),
};
}
public get clipboard() {
return clipboard;
}
@@ -49,8 +48,6 @@ export default class PlatformImplementation extends BasePlatformImplementation {
}
public constructor() {
super();
this.components_ = {};
this.joplin_ = {

View File

@@ -19,7 +19,7 @@ export default function(frameWindow: any, onSubmit: Function, onDismiss: Functio
// Disable enter key from submitting when a text area is in focus!
// https://github.com/laurent22/joplin/issues/4766
//
if (frameWindow.document.activeElement.tagName !== 'TEXTAREA') {
if (frameWindow.document.activeElement.tagName != 'TEXTAREA') {
if (onSubmit) onSubmit();
}
}

View File

@@ -5,7 +5,7 @@ let perFieldReverse: { [field: string]: boolean } = null;
export const notesSortOrderFieldArray = (): string[] => {
// The order of the fields is strictly determinate.
if (fields === null) {
if (fields == null) {
fields = Setting.enumOptionValues('notes.sortOrder.field').sort().reverse();
}
return fields;

View File

@@ -13,65 +13,46 @@ function fileIsNewerThan(path1, path2) {
return stat1.mtime > stat2.mtime;
}
function convertJsx(paths) {
function convertJsx(path) {
chdir(`${__dirname}/..`);
paths.forEach(path => {
fs.readdirSync(path).forEach((filename) => {
const jsxPath = `${path}/${filename}`;
const p = jsxPath.split('.');
if (p.length <= 1) return;
const ext = p[p.length - 1];
if (ext !== 'jsx') return;
p.pop();
fs.readdirSync(path).forEach((filename) => {
const jsxPath = `${path}/${filename}`;
const p = jsxPath.split('.');
if (p.length <= 1) return;
const ext = p[p.length - 1];
if (ext !== 'jsx') return;
p.pop();
const basePath = p.join('.');
const basePath = p.join('.');
const jsPath = `${basePath}.min.js`;
const jsPath = `${basePath}.min.js`;
if (fileIsNewerThan(jsxPath, jsPath)) {
console.info(`Compiling ${jsxPath}...`);
if (fileIsNewerThan(jsxPath, jsPath)) {
console.info(`Compiling ${jsxPath}...`);
// { shell: true } is needed to get it working on Windows:
// https://discourse.joplinapp.org/t/attempting-to-build-on-windows/22559/12
const result = spawnSync('yarn', ['run', 'babel', '--presets', 'react', '--out-file', jsPath, jsxPath], { shell: true });
if (result.status !== 0) {
const msg = [];
if (result.stdout) msg.push(result.stdout.toString());
if (result.stderr) msg.push(result.stderr.toString());
console.error(msg.join('\n'));
if (result.error) console.error(result.error);
process.exit(result.status);
}
// { shell: true } is needed to get it working on Windows:
// https://discourse.joplinapp.org/t/attempting-to-build-on-windows/22559/12
const result = spawnSync('yarn', ['run', 'babel', '--presets', 'react', '--out-file', jsPath, jsxPath], { shell: true });
if (result.status !== 0) {
const msg = [];
if (result.stdout) msg.push(result.stdout.toString());
if (result.stderr) msg.push(result.stderr.toString());
console.error(msg.join('\n'));
if (result.error) console.error(result.error);
process.exit(result.status);
}
});
}
});
}
function build(path) {
chdir(path);
const result = spawnSync('yarn', ['run', 'build'], { shell: true });
if (result.status !== 0) {
const msg = [];
if (result.stdout) msg.push(result.stdout.toString());
if (result.stderr) msg.push(result.stderr.toString());
console.error(msg.join('\n'));
if (result.error) console.error(result.error);
process.exit(result.status);
}
}
module.exports = function() {
convertJsx([
`${__dirname}/../gui`,
`${__dirname}/../gui/MainScreen`,
`${__dirname}/../gui/NoteList`,
`${__dirname}/../plugins`,
]);
build(`${__dirname}/../../pdf-viewer`);
convertJsx(`${__dirname}/../gui`);
convertJsx(`${__dirname}/../gui/MainScreen`);
convertJsx(`${__dirname}/../gui/NoteList`);
convertJsx(`${__dirname}/../plugins`);
// TODO: should get from node_modules @joplin/lib
const libContent = [
fs.readFileSync(`${basePath}/packages/lib/string-utils-common.js`, 'utf8'),
fs.readFileSync(`${basePath}/packages/lib/markJsUtils.js`, 'utf8'),

View File

@@ -72,10 +72,6 @@ async function main() {
src: langSourceDir,
dest: `${buildLibDir}/tinymce/langs`,
},
{
src: resolve(__dirname, '../../pdf-viewer/dist'),
dest: `${buildLibDir}/@joplin/pdf-viewer`,
},
];
const files = [
@@ -87,14 +83,12 @@ async function main() {
'codemirror/addon/dialog/dialog.css',
'@joeattardi/emoji-button/dist/index.js',
'mark.js/dist/mark.min.js',
'tabulator-tables/dist/css/tabulator.min.css',
'tabulator-tables/dist/js/tabulator.min.js',
{
src: resolve(__dirname, '../../lib/services/plugins/sandboxProxy.js'),
dest: `${buildLibDir}/@joplin/lib/services/plugins/sandboxProxy.js`,
},
{
src: resolve(__dirname, '../../pdf-viewer/index.html'),
dest: `${buildLibDir}/@joplin/pdf-viewer/index.html`,
},
];
// First we delete all the destination directories, then we copy the files.

View File

@@ -6,7 +6,7 @@ const execCommand = function(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout) => {
if (error) {
if (error.signal === 'SIGTERM') {
if (error.signal == 'SIGTERM') {
resolve('Process was killed');
} else {
reject(error);

View File

@@ -8,7 +8,7 @@ function execCommand(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
if (error.signal === 'SIGTERM') {
if (error.signal == 'SIGTERM') {
resolve('Process was killed');
} else {
reject(new Error([stdout.trim(), stderr.trim()].join('\n')));

View File

@@ -63,7 +63,5 @@ buck-out/
lib/csstojs/
lib/rnInjectedJs/
dist/
components/NoteEditor/CodeMirror/CodeMirror.bundle.js
components/NoteEditor/CodeMirror/CodeMirror.bundle.min.js
utils/fs-driver-android.js
components/NoteEditor/CodeMirror.bundle.js
components/NoteEditor/CodeMirror.bundle.min.js

View File

@@ -146,8 +146,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097669
versionName "2.9.1"
versionCode 2097668
versionName "2.8.1"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -38,10 +38,9 @@ class Dropdown extends React.Component {
const listTop = Math.min(maxListTop, this.state.headerSize.y + this.state.headerSize.height);
const wrapperStyle = {
width: this.state.headerSize.width,
height: listHeight + 2, // +2 for the border (otherwise it makes the scrollbar appear)
marginTop: listTop,
marginLeft: this.state.headerSize.x,
alignSelf: 'center',
};
const itemListStyle = Object.assign({}, this.props.itemListStyle ? this.props.itemListStyle : {}, {
@@ -87,6 +86,7 @@ class Dropdown extends React.Component {
if (this.props.labelTransform && this.props.labelTransform === 'trim') headerLabel = headerLabel.trim();
const closeList = () => {
if (this.props.onClose) this.props.onClose();
this.setState({ listVisible: false });
};
@@ -116,6 +116,7 @@ class Dropdown extends React.Component {
onPress={() => {
this.updateHeaderCoordinates();
this.setState({ listVisible: true });
if (this.props.onOpen) this.props.onOpen();
}}
>
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>

View File

@@ -139,17 +139,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
js.push('}');
js.push('true;');
// iOS doesn't automatically adjust the WebView's font size to match users'
// accessibility settings. To do this, we need to tell it to match the system font.
// See https://github.com/ionic-team/capacitor/issues/2748#issuecomment-612923135
const iOSSpecificCss = `
@media screen {
:root body {
font: -apple-system-body;
}
}
`;
html =
`
<!DOCTYPE html>
@@ -157,9 +146,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
</style>
${assetsToHeaders(result.pluginAssets, { asHtml: true })}
</head>
<body>

View File

@@ -0,0 +1,219 @@
/* eslint-disable import/prefer-default-export */
// This contains the CodeMirror instance, which needs to be built into a bundle
// using `npm run buildInjectedJs`. This bundle is then loaded from
// NoteEditor.tsx into the webview.
//
// In general, since this file is harder to debug due to the intermediate built
// step, it's better to keep it as light as possible - it shoud just be a light
// wrapper to access CodeMirror functionalities. Anything else should be done
// from NoteEditor.tsx.
import { EditorState, Extension } from '@codemirror/state';
import { EditorView, drawSelection, highlightSpecialChars, ViewUpdate } from '@codemirror/view';
import { markdown } from '@codemirror/lang-markdown';
import { defaultHighlightStyle, HighlightStyle, tags } from '@codemirror/highlight';
import { undo, redo, history, undoDepth, redoDepth } from '@codemirror/history';
interface CodeMirrorResult {
editor: EditorView;
undo: Function;
redo: Function;
select: (anchor: number, head: number)=> void;
insertText: (text: string)=> void;
}
function postMessage(name: string, data: any) {
(window as any).ReactNativeWebView.postMessage(JSON.stringify({
data,
name,
}));
}
function logMessage(...msg: any[]) {
postMessage('onLog', { value: msg });
}
// For an example on how to customize the theme, see:
//
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
//
// For a tutorial, see:
//
// https://codemirror.net/6/examples/styling/#themes
//
// Use Safari developer tools to view the content of the CodeMirror iframe while
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
// use '&.cm-focused' in the theme.
const createTheme = (theme: any): Extension => {
const isDarkTheme = theme.appearance === 'dark';
const baseGlobalStyle: Record<string, string> = {
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
fontSize: `${theme.fontSize}px`,
};
const baseCursorStyle: Record<string, string> = { };
const baseContentStyle: Record<string, string> = { };
const baseSelectionStyle: Record<string, string> = { };
// If we're in dark mode, the caret and selection are difficult to see.
// Adjust them appropriately
if (isDarkTheme) {
// Styling the caret requires styling both the caret itself
// and the CodeMirror caret.
// See https://codemirror.net/6/examples/styling/#themes
baseContentStyle.caretColor = 'white';
baseCursorStyle.borderLeftColor = 'white';
baseSelectionStyle.backgroundColor = '#6b6b6b';
}
const baseTheme = EditorView.baseTheme({
'&': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
'.cm-content': baseContentStyle,
'&.cm-focused .cm-cursor': baseCursorStyle,
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
'&.cm-focused': {
outline: 'none',
},
});
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
const baseHeadingStyle = {
fontWeight: 'bold',
fontFamily: theme.fontFamily,
};
const syntaxHighlighting = HighlightStyle.define([
{
tag: tags.strong,
fontWeight: 'bold',
},
{
tag: tags.emphasis,
fontStyle: 'italic',
},
{
...baseHeadingStyle,
tag: tags.heading1,
fontSize: '1.6em',
borderBottom: `1px solid ${theme.dividerColor}`,
},
{
...baseHeadingStyle,
tag: tags.heading2,
fontSize: '1.4em',
},
{
...baseHeadingStyle,
tag: tags.heading3,
fontSize: '1.3em',
},
{
...baseHeadingStyle,
tag: tags.heading4,
fontSize: '1.2em',
},
{
...baseHeadingStyle,
tag: tags.heading5,
fontSize: '1.1em',
},
{
...baseHeadingStyle,
tag: tags.heading6,
fontSize: '1.0em',
},
{
tag: tags.list,
fontFamily: theme.fontFamily,
},
]);
return [
baseTheme,
appearanceTheme,
syntaxHighlighting,
];
};
export function initCodeMirror(parentElement: any, initialText: string, theme: any): CodeMirrorResult {
logMessage('Initializing CodeMirror...');
let schedulePostUndoRedoDepthChangeId_: any = 0;
function schedulePostUndoRedoDepthChange(editor: EditorView, doItNow: boolean = false) {
if (schedulePostUndoRedoDepthChangeId_) {
if (doItNow) {
clearTimeout(schedulePostUndoRedoDepthChangeId_);
} else {
return;
}
}
schedulePostUndoRedoDepthChangeId_ = setTimeout(() => {
schedulePostUndoRedoDepthChangeId_ = null;
postMessage('onUndoRedoDepthChange', {
undoDepth: undoDepth(editor.state),
redoDepth: redoDepth(editor.state),
});
}, doItNow ? 0 : 1000);
}
const editor = new EditorView({
state: EditorState.create({
extensions: [
markdown(),
createTheme(theme),
history(),
drawSelection(),
highlightSpecialChars(),
EditorView.lineWrapping,
EditorView.contentAttributes.of({ autocapitalize: 'sentence' }),
defaultHighlightStyle.fallback,
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged) {
postMessage('onChange', { value: editor.state.doc.toString() });
schedulePostUndoRedoDepthChange(editor);
}
if (!viewUpdate.state.selection.eq(viewUpdate.startState.selection)) {
const mainRange = viewUpdate.state.selection.main;
const selStart = mainRange.from;
const selEnd = mainRange.to;
postMessage('onSelectionChange', { selection: { start: selStart, end: selEnd } });
}
}),
],
doc: initialText,
}),
parent: parentElement,
});
return {
editor,
undo: () => {
undo(editor);
schedulePostUndoRedoDepthChange(editor, true);
},
redo: () => {
redo(editor);
schedulePostUndoRedoDepthChange(editor, true);
},
select: (anchor: number, head: number) => {
editor.dispatch(editor.state.update({
selection: { anchor, head },
scrollIntoView: true,
}));
},
insertText: (text: string) => {
editor.dispatch(editor.state.replaceSelection(text));
},
};
}

View File

@@ -1,428 +0,0 @@
/* eslint-disable import/prefer-default-export */
// This contains the CodeMirror instance, which needs to be built into a bundle
// using `npm run buildInjectedJs`. This bundle is then loaded from
// NoteEditor.tsx into the webview.
//
// In general, since this file is harder to debug due to the intermediate built
// step, it's better to keep it as light as possible - it shoud just be a light
// wrapper to access CodeMirror functionalities. Anything else should be done
// from NoteEditor.tsx.
import { MarkdownMathExtension } from './markdownMathParser';
import createTheme from './theme';
import decoratorExtension from './decoratorExtension';
import syntaxHighlightingLanguages from './syntaxHighlightingLanguages';
import { EditorState } from '@codemirror/state';
import { markdown } from '@codemirror/lang-markdown';
import { GFM as GitHubFlavoredMarkdownExtension } from '@lezer/markdown';
import { indentOnInput, indentUnit, syntaxTree } from '@codemirror/language';
import {
openSearchPanel, closeSearchPanel, SearchQuery, setSearchQuery, getSearchQuery,
highlightSelectionMatches, search, findNext, findPrevious, replaceAll, replaceNext,
} from '@codemirror/search';
import {
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command,
} from '@codemirror/view';
import { undo, redo, history, undoDepth, redoDepth, indentWithTab } from '@codemirror/commands';
import { keymap, KeyBinding } from '@codemirror/view';
import { searchKeymap } from '@codemirror/search';
import { historyKeymap, defaultKeymap } from '@codemirror/commands';
import { CodeMirrorControl } from './types';
import { EditorSettings, ListType, SearchState } from '../types';
import { ChangeEvent, SelectionChangeEvent, Selection } from '../types';
import SelectionFormatting from '../SelectionFormatting';
import { logMessage, postMessage } from './webviewLogger';
import {
decreaseIndent, increaseIndent,
toggleBolded, toggleCode,
toggleHeaderLevel, toggleItalicized,
toggleList, toggleMath, updateLink,
} from './markdownCommands';
export function initCodeMirror(
parentElement: any, initialText: string, settings: EditorSettings
): CodeMirrorControl {
logMessage('Initializing CodeMirror...');
const theme = settings.themeData;
let searchVisible = false;
let schedulePostUndoRedoDepthChangeId_: any = 0;
const schedulePostUndoRedoDepthChange = (editor: EditorView, doItNow: boolean = false) => {
if (schedulePostUndoRedoDepthChangeId_) {
if (doItNow) {
clearTimeout(schedulePostUndoRedoDepthChangeId_);
} else {
return;
}
}
schedulePostUndoRedoDepthChangeId_ = setTimeout(() => {
schedulePostUndoRedoDepthChangeId_ = null;
postMessage('onUndoRedoDepthChange', {
undoDepth: undoDepth(editor.state),
redoDepth: redoDepth(editor.state),
});
}, doItNow ? 0 : 1000);
};
const notifyDocChanged = (viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged) {
const event: ChangeEvent = {
value: editor.state.doc.toString(),
};
postMessage('onChange', event);
schedulePostUndoRedoDepthChange(editor);
}
};
const notifyLinkEditRequest = () => {
postMessage('onRequestLinkEdit', null);
};
const showSearchDialog = () => {
const query = getSearchQuery(editor.state);
const searchState: SearchState = {
searchText: query.search,
replaceText: query.replace,
useRegex: query.regexp,
caseSensitive: query.caseSensitive,
dialogVisible: true,
};
postMessage('onRequestShowSearch', searchState);
searchVisible = true;
};
const hideSearchDialog = () => {
postMessage('onRequestHideSearch', null);
searchVisible = false;
};
const notifySelectionChange = (viewUpdate: ViewUpdate) => {
if (!viewUpdate.state.selection.eq(viewUpdate.startState.selection)) {
const mainRange = viewUpdate.state.selection.main;
const selection: Selection = {
start: mainRange.from,
end: mainRange.to,
};
const event: SelectionChangeEvent = {
selection,
};
postMessage('onSelectionChange', event);
}
};
const notifySelectionFormattingChange = (viewUpdate?: ViewUpdate) => {
// If we can't determine the previous formatting, post the update regardless
if (!viewUpdate) {
const formatting = computeSelectionFormatting(editor.state);
postMessage('onSelectionFormattingChange', formatting.toJSON());
} else if (viewUpdate.docChanged || !viewUpdate.state.selection.eq(viewUpdate.startState.selection)) {
// Only post the update if something changed
const oldFormatting = computeSelectionFormatting(viewUpdate.startState);
const newFormatting = computeSelectionFormatting(viewUpdate.state);
if (!oldFormatting.eq(newFormatting)) {
postMessage('onSelectionFormattingChange', newFormatting.toJSON());
}
}
};
const computeSelectionFormatting = (state: EditorState): SelectionFormatting => {
const range = state.selection.main;
const formatting: SelectionFormatting = new SelectionFormatting();
formatting.selectedText = state.doc.sliceString(range.from, range.to);
formatting.spellChecking = editor.contentDOM.spellcheck;
const parseLinkData = (nodeText: string) => {
const linkMatch = nodeText.match(/\[([^\]]*)\]\(([^)]*)\)/);
if (linkMatch) {
return {
linkText: linkMatch[1],
linkURL: linkMatch[2],
};
}
return null;
};
// Find nodes that overlap/are within the selected region
syntaxTree(state).iterate({
from: range.from, to: range.to,
enter: node => {
// Checklists don't have a specific containing node. As such,
// we're in a checklist if we've selected a 'Task' node.
if (node.name === 'Task') {
formatting.inChecklist = true;
}
// Only handle notes that contain the entire range.
if (node.from > range.from || node.to < range.to) {
return;
}
// Lazily compute the node's text
const nodeText = () => state.doc.sliceString(node.from, node.to);
switch (node.name) {
case 'StrongEmphasis':
formatting.bolded = true;
break;
case 'Emphasis':
formatting.italicized = true;
break;
case 'ListItem':
formatting.listLevel += 1;
break;
case 'BulletList':
formatting.inUnorderedList = true;
break;
case 'OrderedList':
formatting.inOrderedList = true;
break;
case 'TaskList':
formatting.inChecklist = true;
break;
case 'InlineCode':
case 'FencedCode':
formatting.inCode = true;
formatting.unspellCheckableRegion = true;
break;
case 'InlineMath':
case 'BlockMath':
formatting.inMath = true;
formatting.unspellCheckableRegion = true;
break;
case 'ATXHeading1':
formatting.headerLevel = 1;
break;
case 'ATXHeading2':
formatting.headerLevel = 2;
break;
case 'ATXHeading3':
formatting.headerLevel = 3;
break;
case 'ATXHeading4':
formatting.headerLevel = 4;
break;
case 'ATXHeading5':
formatting.headerLevel = 5;
break;
case 'URL':
formatting.inLink = true;
formatting.linkData.linkURL = nodeText();
formatting.unspellCheckableRegion = true;
break;
case 'Link':
formatting.inLink = true;
formatting.linkData = parseLinkData(nodeText());
break;
}
},
});
// The markdown parser marks checklists as unordered lists. Ensure
// that they aren't marked as such.
if (formatting.inChecklist) {
if (!formatting.inUnorderedList) {
// Even if the selection contains a Task, because an unordered list node
// must contain a valid Task node, we're only in a checklist if we're also in
// an unordered list.
formatting.inChecklist = false;
} else {
formatting.inUnorderedList = false;
}
}
if (formatting.unspellCheckableRegion) {
formatting.spellChecking = false;
}
return formatting;
};
// Returns a keyboard command that returns true (so accepts the keybind)
const keyCommand = (key: string, run: Command): KeyBinding => {
return {
key,
run,
preventDefault: true,
};
};
const editor = new EditorView({
state: EditorState.create({
// See https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
// for a sample configuration.
extensions: [
markdown({
extensions: [
GitHubFlavoredMarkdownExtension,
// Don't highlight KaTeX if the user disabled it
settings.katexEnabled ? MarkdownMathExtension : [],
],
codeLanguages: syntaxHighlightingLanguages,
}),
...createTheme(theme),
history(),
search({
createPanel(_: EditorView) {
return {
// The actual search dialog is implemented with react native,
// use a dummy element.
dom: document.createElement('div'),
mount() {
showSearchDialog();
},
destroy() {
hideSearchDialog();
},
};
},
}),
drawSelection(),
highlightSpecialChars(),
highlightSelectionMatches(),
indentOnInput(),
// By default, indent with four spaces
indentUnit.of(' '),
EditorState.tabSize.of(4),
// Apply styles to entire lines (block-display decorations)
decoratorExtension,
EditorView.lineWrapping,
EditorView.contentAttributes.of({ autocapitalize: 'sentence' }),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
notifyDocChanged(viewUpdate);
notifySelectionChange(viewUpdate);
notifySelectionFormattingChange(viewUpdate);
}),
keymap.of([
// Custom mod-f binding: Toggle the external dialog implementation
// (don't show/hide the Panel dialog).
keyCommand('Mod-f', (_: EditorView) => {
if (searchVisible) {
hideSearchDialog();
} else {
showSearchDialog();
}
return true;
}),
// Markdown formatting keyboard shortcuts
keyCommand('Mod-b', toggleBolded),
keyCommand('Mod-i', toggleItalicized),
keyCommand('Mod-$', toggleMath),
keyCommand('Mod-`', toggleCode),
keyCommand('Mod-[', decreaseIndent),
keyCommand('Mod-]', increaseIndent),
keyCommand('Mod-k', (_: EditorView) => {
notifyLinkEditRequest();
return true;
}),
...defaultKeymap, ...historyKeymap, indentWithTab, ...searchKeymap,
]),
],
doc: initialText,
}),
parent: parentElement,
});
const updateSearchQuery = (newState: SearchState) => {
const query = new SearchQuery({
search: newState.searchText,
caseSensitive: newState.caseSensitive,
regexp: newState.useRegex,
replace: newState.replaceText,
});
editor.dispatch({
effects: setSearchQuery.of(query),
});
};
const editorControls = {
editor,
undo: () => {
undo(editor);
schedulePostUndoRedoDepthChange(editor, true);
},
redo: () => {
redo(editor);
schedulePostUndoRedoDepthChange(editor, true);
},
select: (anchor: number, head: number) => {
editor.dispatch(editor.state.update({
selection: { anchor, head },
scrollIntoView: true,
}));
},
scrollSelectionIntoView: () => {
editor.dispatch(editor.state.update({
scrollIntoView: true,
}));
},
insertText: (text: string) => {
editor.dispatch(editor.state.replaceSelection(text));
},
toggleFindDialog: () => {
const opened = openSearchPanel(editor);
if (!opened) {
closeSearchPanel(editor);
}
},
setSpellcheckEnabled: (enabled: boolean) => {
editor.contentDOM.spellcheck = enabled;
notifySelectionFormattingChange();
},
// Formatting
toggleBolded: () => { toggleBolded(editor); },
toggleItalicized: () => { toggleItalicized(editor); },
toggleCode: () => { toggleCode(editor); },
toggleMath: () => { toggleMath(editor); },
increaseIndent: () => { increaseIndent(editor); },
decreaseIndent: () => { decreaseIndent(editor); },
toggleList: (kind: ListType) => { toggleList(kind)(editor); },
toggleHeaderLevel: (level: number) => { toggleHeaderLevel(level)(editor); },
updateLink: (label: string, url: string) => { updateLink(label, url)(editor); },
// Search
searchControl: {
findNext: () => {
findNext(editor);
},
findPrevious: () => {
findPrevious(editor);
},
replaceCurrent: () => {
replaceNext(editor);
},
replaceAll: () => {
replaceAll(editor);
},
setSearchState: (state: SearchState) => {
updateSearchQuery(state);
},
showSearch: () => {
showSearchDialog();
},
hideSearch: () => {
hideSearchDialog();
},
},
};
return editorControls;
}

View File

@@ -1,23 +0,0 @@
import { markdown } from '@codemirror/lang-markdown';
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
import { indentUnit } from '@codemirror/language';
import { SelectionRange, EditorSelection, EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { MarkdownMathExtension } from './markdownMathParser';
// Creates and returns a minimal editor with markdown extensions
const createEditor = (initialText: string, initialSelection: SelectionRange): EditorView => {
return new EditorView({
doc: initialText,
selection: EditorSelection.create([initialSelection]),
extensions: [
markdown({
extensions: [MarkdownMathExtension, GithubFlavoredMarkdownExt],
}),
indentUnit.of('\t'),
EditorState.tabSize.of(4),
],
});
};
export default createEditor;

View File

@@ -1,170 +0,0 @@
//
// Exports an editor plugin that creates multi-line decorations based on the
// editor's syntax tree (assumes markdown).
//
// For more about creating decorations, see https://codemirror.net/examples/zebra/
//
import { Decoration, EditorView } from '@codemirror/view';
import { ViewPlugin, DecorationSet, ViewUpdate } from '@codemirror/view';
import { ensureSyntaxTree } from '@codemirror/language';
import { RangeSetBuilder } from '@codemirror/state';
const regionStartDecoration = Decoration.line({
attributes: { class: 'cm-regionFirstLine' },
});
const regionStopDecoration = Decoration.line({
attributes: { class: 'cm-regionLastLine' },
});
const codeBlockDecoration = Decoration.line({
attributes: { class: 'cm-codeBlock', spellcheck: 'false' },
});
const inlineCodeDecoration = Decoration.mark({
attributes: { class: 'cm-inlineCode', spellcheck: 'false' },
});
const mathBlockDecoration = Decoration.line({
attributes: { class: 'cm-mathBlock', spellcheck: 'false' },
});
const inlineMathDecoration = Decoration.mark({
attributes: { class: 'cm-inlineMath', spellcheck: 'false' },
});
const urlDecoration = Decoration.mark({
attributes: { class: 'cm-url', spellcheck: 'false' },
});
const blockQuoteDecoration = Decoration.line({
attributes: { class: 'cm-blockQuote' },
});
const headerLineDecoration = Decoration.line({
attributes: { class: 'cm-headerLine' },
});
type DecorationDescription = { pos: number; length?: number; decoration: Decoration };
// Returns a set of [Decoration]s, associated with block syntax groups that require
// full-line styling.
const computeDecorations = (view: EditorView) => {
const decorations: DecorationDescription[] = [];
// Add a decoration to all lines between the document position [from] up to
// and includeing the position [to].
const addDecorationToLines = (from: number, to: number, decoration: Decoration) => {
let pos = from;
while (pos <= to) {
const line = view.state.doc.lineAt(pos);
decorations.push({
pos: line.from,
decoration,
});
// Move to the next line
pos = line.to + 1;
}
};
const addDecorationToRange = (from: number, to: number, decoration: Decoration) => {
decorations.push({
pos: from,
length: to - from,
decoration,
});
};
for (const { from, to } of view.visibleRanges) {
ensureSyntaxTree(
view.state,
to
)?.iterate({
from, to,
enter: node => {
let blockDecorated = false;
// Compute the visible region of the node.
const viewFrom = Math.max(from, node.from);
const viewTo = Math.min(to, node.to);
switch (node.name) {
case 'FencedCode':
case 'CodeBlock':
addDecorationToLines(viewFrom, viewTo, codeBlockDecoration);
blockDecorated = true;
break;
case 'BlockMath':
addDecorationToLines(viewFrom, viewTo, mathBlockDecoration);
blockDecorated = true;
break;
case 'Blockquote':
addDecorationToLines(viewFrom, viewTo, blockQuoteDecoration);
blockDecorated = true;
break;
case 'InlineMath':
addDecorationToRange(viewFrom, viewTo, inlineMathDecoration);
break;
case 'InlineCode':
addDecorationToRange(viewFrom, viewTo, inlineCodeDecoration);
break;
case 'URL':
addDecorationToRange(viewFrom, viewTo, urlDecoration);
break;
case 'SetextHeading1':
case 'SetextHeading2':
case 'ATXHeading1':
case 'ATXHeading2':
case 'ATXHeading3':
case 'ATXHeading4':
case 'ATXHeading5':
case 'ATXHeading6':
addDecorationToLines(viewFrom, viewTo, headerLineDecoration);
break;
}
// Only block decorations will have differing first and last lines
if (blockDecorated) {
// Allow different styles for the first, last lines in a block.
if (viewFrom === node.from) {
addDecorationToLines(viewFrom, viewFrom, regionStartDecoration);
}
if (viewTo === node.to) {
addDecorationToLines(viewTo, viewTo, regionStopDecoration);
}
}
},
});
}
decorations.sort((a, b) => a.pos - b.pos);
// Items need to be added to a RangeSetBuilder in ascending order
const decorationBuilder = new RangeSetBuilder<Decoration>();
for (const { pos, length, decoration } of decorations) {
// Null length => entire line
decorationBuilder.add(pos, pos + (length ?? 0), decoration);
}
return decorationBuilder.finish();
};
const decoratorExtension = ViewPlugin.fromClass(class {
public decorations: DecorationSet;
public constructor(view: EditorView) {
this.decorations = computeDecorations(view);
}
public update(viewUpdate: ViewUpdate) {
if (viewUpdate.docChanged || viewUpdate.viewportChanged) {
this.decorations = computeDecorations(viewUpdate.view);
}
}
}, {
decorations: pluginVal => pluginVal.decorations,
});
export default decoratorExtension;

View File

@@ -1,48 +0,0 @@
<!--
Open this file in a web browser to more easily debug the CodeMirror editor.
Messages will show up in the console when posted.
-->
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<meta charset="utf-8"/>
<title>CodeMirror test</title>
</head>
<body>
<div class="CodeMirror"></div>
<script>
// Override the default postMessage — codeMirrorBundle expects
// this to be present.
window.ReactNativeWebView = {
postMessage: message => {
console.log('postMessage:', message);
},
};
</script>
<script src="./CodeMirror.bundle.js"></script>
<script>
const parent = document.querySelector('.CodeMirror');
const initialText = 'Testing...';
const settings = {
katexEnabled: true,
themeData: {
fontSize: 1, // em
fontFamily: 'serif',
backgroundColor: 'black',
color: 'white',
backgroundColor2: '#330',
color2: '#ff0',
backgroundColor3: '#404',
color3: '#f0f',
backgroundColor4: '#555',
color4: '#0ff',
appearance: 'dark',
},
};
codeMirrorBundle.initCodeMirror(parent, initialText, settings);
</script>
</body>
</html>

View File

@@ -1,47 +0,0 @@
/**
* @jest-environment jsdom
*/
import { EditorSelection } from '@codemirror/state';
import { ListType } from '../types';
import createEditor from './createEditor';
import { toggleList } from './markdownCommands';
describe('markdownCommands.bulletedVsChecklist', () => {
const bulletedListPart = '- Test\n- This is a test.\n- 3\n- 4\n- 5';
const checklistPart = '- [ ] This is a checklist\n- [ ] with multiple items.\n- [ ] ☑';
const initialDocText = `${bulletedListPart}\n\n${checklistPart}`;
it('should remove a checklist following a bulleted list without modifying the bulleted list', () => {
const editor = createEditor(
initialDocText, EditorSelection.cursor(bulletedListPart.length + 5)
);
toggleList(ListType.CheckList)(editor);
expect(editor.state.doc.toString()).toBe(
`${bulletedListPart}\n\nThis is a checklist\nwith multiple items.\n☑`
);
});
it('should remove an unordered list following a checklist without modifying the checklist', () => {
const editor = createEditor(
initialDocText, EditorSelection.cursor(bulletedListPart.length - 5)
);
toggleList(ListType.UnorderedList)(editor);
expect(editor.state.doc.toString()).toBe(
`Test\nThis is a test.\n3\n4\n5\n\n${checklistPart}`
);
});
it('should replace a selection of unordered and task lists with a correctly-numbered list', () => {
const editor = createEditor(
initialDocText, EditorSelection.range(0, initialDocText.length)
);
toggleList(ListType.OrderedList)(editor);
expect(editor.state.doc.toString()).toBe(
'1. Test\n2. This is a test.\n3. 3\n4. 4\n5. 5'
+ '\n\n6. This is a checklist\n7. with multiple items.\n8. ☑'
);
});
});

View File

@@ -1,248 +0,0 @@
/**
* @jest-environment jsdom
*/
import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import {
toggleBolded, toggleCode, toggleHeaderLevel, toggleItalicized, toggleMath, updateLink,
} from './markdownCommands';
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
import { markdown } from '@codemirror/lang-markdown';
import { MarkdownMathExtension } from './markdownMathParser';
import { indentUnit } from '@codemirror/language';
// Creates and returns a minimal editor with markdown extensions
const createEditor = (initialText: string, initialSelection: SelectionRange): EditorView => {
return new EditorView({
doc: initialText,
selection: EditorSelection.create([initialSelection]),
extensions: [
markdown({
extensions: [MarkdownMathExtension, GithubFlavoredMarkdownExt],
}),
indentUnit.of('\t'),
EditorState.tabSize.of(4),
],
});
};
describe('markdownCommands', () => {
it('should bold/italicize everything selected', () => {
const initialDocText = 'Testing...';
const editor = createEditor(
initialDocText, EditorSelection.range(0, initialDocText.length)
);
toggleBolded(editor);
let mainSel = editor.state.selection.main;
const boldedText = '**Testing...**';
expect(editor.state.doc.toString()).toBe(boldedText);
expect(mainSel.from).toBe(0);
expect(mainSel.to).toBe(boldedText.length);
toggleBolded(editor);
mainSel = editor.state.selection.main;
expect(editor.state.doc.toString()).toBe(initialDocText);
expect(mainSel.from).toBe(0);
expect(mainSel.to).toBe(initialDocText.length);
toggleItalicized(editor);
expect(editor.state.doc.toString()).toBe('*Testing...*');
toggleItalicized(editor);
expect(editor.state.doc.toString()).toBe('Testing...');
});
it('toggling math should both create and navigate out of math regions', () => {
const initialDocText = 'Testing... ';
const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length));
toggleMath(editor);
expect(editor.state.doc.toString()).toBe('Testing... $$');
expect(editor.state.selection.main.empty).toBe(true);
editor.dispatch(editor.state.replaceSelection('3 + 3 \\neq 5'));
expect(editor.state.doc.toString()).toBe('Testing... $3 + 3 \\neq 5$');
toggleMath(editor);
editor.dispatch(editor.state.replaceSelection('...'));
expect(editor.state.doc.toString()).toBe('Testing... $3 + 3 \\neq 5$...');
});
it('toggling inline code should both create and navigate out of an inline code region', () => {
const initialDocText = 'Testing...\n\n';
const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length));
toggleCode(editor);
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
toggleCode(editor);
editor.dispatch(editor.state.replaceSelection(' is a function.'));
expect(editor.state.doc.toString()).toBe('Testing...\n\n`f(x) = ...` is a function.');
});
it('should set headers to the proper levels (when toggling)', () => {
const initialDocText = 'Testing...\nThis is a test.';
const editor = createEditor(initialDocText, EditorSelection.cursor(3));
toggleHeaderLevel(1)(editor);
let mainSel = editor.state.selection.main;
expect(editor.state.doc.toString()).toBe('# Testing...\nThis is a test.');
expect(mainSel.empty).toBe(true);
expect(mainSel.from).toBe('# Testing...'.length);
toggleHeaderLevel(2)(editor);
mainSel = editor.state.selection.main;
expect(editor.state.doc.toString()).toBe('## Testing...\nThis is a test.');
expect(mainSel.empty).toBe(true);
expect(mainSel.from).toBe('## Testing...'.length);
toggleHeaderLevel(2)(editor);
mainSel = editor.state.selection.main;
expect(editor.state.doc.toString()).toEqual(initialDocText);
expect(mainSel.empty).toBe(true);
expect(mainSel.from).toBe('Testing...'.length);
});
it('headers should toggle properly within block quotes', () => {
const initialDocText = 'Testing...\n\n> This is a test.\n> ...a test';
const editor = createEditor(
initialDocText,
EditorSelection.cursor('Testing...\n\n> This'.length)
);
toggleHeaderLevel(1)(editor);
const mainSel = editor.state.selection.main;
expect(editor.state.doc.toString()).toBe(
'Testing...\n\n> # This is a test.\n> ...a test'
);
expect(mainSel.empty).toBe(true);
expect(mainSel.from).toBe('Testing...\n\n> # This is a test.'.length);
toggleHeaderLevel(3)(editor);
expect(editor.state.doc.toString()).toBe(
'Testing...\n\n> ### This is a test.\n> ...a test'
);
});
it('block math should properly toggle within block quotes', () => {
const initialDocText = 'Testing...\n\n> This is a test.\n> y = mx + b\n> ...a test';
const editor = createEditor(
initialDocText,
EditorSelection.range(
'Testing...\n\n> This'.length,
'Testing...\n\n> This is a test.\n> y = mx + b'.length
)
);
toggleMath(editor);
// Toggling math should surround the content in '$$'s
let mainSel = editor.state.selection.main;
expect(editor.state.doc.toString()).toEqual(
'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test'
);
expect(mainSel.from).toBe('Testing...\n\n'.length);
expect(mainSel.to).toBe('Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$'.length);
// Change to a cursor --- test cursor expansion
editor.dispatch({
selection: EditorSelection.cursor('Testing...\n\n> $$\n> This is'.length),
});
// Toggling math again should remove the '$$'s
toggleMath(editor);
mainSel = editor.state.selection.main;
expect(editor.state.doc.toString()).toEqual(initialDocText);
expect(mainSel.from).toBe('Testing...\n\n'.length);
expect(mainSel.to).toBe('Testing...\n\n> This is a test.\n> y = mx + b'.length);
});
it('updateLink should replace link titles and isolate URLs if no title is given', () => {
const initialDocText = '[foo](http://example.com/)';
const editor = createEditor(initialDocText, EditorSelection.cursor('[f'.length));
updateLink('bar', 'https://example.com/')(editor);
expect(editor.state.doc.toString()).toBe(
'[bar](https://example.com/)'
);
updateLink('', 'https://example.com/')(editor);
expect(editor.state.doc.toString()).toBe(
'https://example.com/'
);
});
it('toggling math twice, starting on a line with content, should a math block', () => {
const initialDocText = 'Testing... ';
const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length));
toggleMath(editor);
toggleMath(editor);
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
expect(editor.state.doc.toString()).toBe('Testing... \n$$\nf(x) = ...\n$$');
});
it('toggling math twice on an empty line should create an empty math block', () => {
const initialDocText = 'Testing...\n\n';
const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length));
toggleMath(editor);
toggleMath(editor);
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
expect(editor.state.doc.toString()).toBe('Testing...\n\n$$\nf(x) = ...\n$$');
});
it('toggling code twice on an empty line should create an empty code block', () => {
const initialDocText = 'Testing...\n\n';
const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length));
// Toggling code twice should create a block code region
toggleCode(editor);
toggleCode(editor);
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
expect(editor.state.doc.toString()).toBe('Testing...\n\n```\nf(x) = ...\n```');
toggleCode(editor);
expect(editor.state.doc.toString()).toBe('Testing...\n\nf(x) = ...\n');
});
it('toggling math twice inside a block quote should produce an empty math block', () => {
const initialDocText = '> Testing...> \n> ';
const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length));
toggleMath(editor);
toggleMath(editor);
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
expect(editor.state.doc.toString()).toBe(
'> Testing...> \n> \n> $$\n> f(x) = ...\n> $$'
);
// If we toggle math again, everything from the start of the line with the first
// $$ to the end of the document should be selected.
toggleMath(editor);
const sel = editor.state.selection.main;
expect(sel.from).toBe('> Testing...> \n> \n'.length);
expect(sel.to).toBe(editor.state.doc.length);
});
it('toggling inline code should both create and navigate out of an inline code region', () => {
const initialDocText = 'Testing...\n\n';
const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length));
toggleCode(editor);
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
toggleCode(editor);
editor.dispatch(editor.state.replaceSelection(' is a function.'));
expect(editor.state.doc.toString()).toBe('Testing...\n\n`f(x) = ...` is a function.');
});
});

View File

@@ -1,189 +0,0 @@
/**
* @jest-environment jsdom
*/
import { EditorSelection, EditorState } from '@codemirror/state';
import {
increaseIndent, toggleList,
} from './markdownCommands';
import { ListType } from '../types';
import createEditor from './createEditor';
describe('markdownCommands.toggleList', () => {
it('should remove the same type of list', () => {
const initialDocText = '- testing\n- this is a test';
const editor = createEditor(
initialDocText,
EditorSelection.cursor(5)
);
toggleList(ListType.UnorderedList)(editor);
expect(editor.state.doc.toString()).toBe(
'testing\nthis is a test'
);
});
it('should insert a numbered list with correct numbering', () => {
const initialDocText = 'Testing...\nThis is a test\nof list toggling...';
const editor = createEditor(
initialDocText,
EditorSelection.cursor('Testing...\nThis is a'.length)
);
toggleList(ListType.OrderedList)(editor);
expect(editor.state.doc.toString()).toBe(
'Testing...\n1. This is a test\nof list toggling...'
);
editor.setState(EditorState.create({
doc: initialDocText,
selection: EditorSelection.range(4, initialDocText.length),
}));
toggleList(ListType.OrderedList)(editor);
expect(editor.state.doc.toString()).toBe(
'1. Testing...\n2. This is a test\n3. of list toggling...'
);
});
const numberedListText = '- 1\n- 2\n- 3\n- 4\n- 5\n- 6\n- 7';
it('should correctly replace an unordered list with a numbered list', () => {
const editor = createEditor(
numberedListText,
EditorSelection.cursor(numberedListText.length)
);
toggleList(ListType.OrderedList)(editor);
expect(editor.state.doc.toString()).toBe(
'1. 1\n2. 2\n3. 3\n4. 4\n5. 5\n6. 6\n7. 7'
);
});
it('should correctly replace an unordered list with a checklist', () => {
const editor = createEditor(
numberedListText,
EditorSelection.cursor(numberedListText.length)
);
toggleList(ListType.CheckList)(editor);
expect(editor.state.doc.toString()).toBe(
'- [ ] 1\n- [ ] 2\n- [ ] 3\n- [ ] 4\n- [ ] 5\n- [ ] 6\n- [ ] 7'
);
});
it('should properly toggle a sublist of a bulleted list', () => {
const preSubListText = '# List test\n * This\n * is\n';
const initialDocText = `${preSubListText}\t* a\n\t* test\n * of list toggling`;
const editor = createEditor(
initialDocText,
EditorSelection.cursor(preSubListText.length + '\t* a'.length)
);
// Indentation should be preserved when changing list types
toggleList(ListType.OrderedList)(editor);
expect(editor.state.doc.toString()).toBe(
'# List test\n * This\n * is\n\t1. a\n\t2. test\n * of list toggling'
);
// The changed region should be selected
expect(editor.state.selection.main.from).toBe(preSubListText.length);
expect(editor.state.selection.main.to).toBe(
`${preSubListText}\t1. a\n\t2. test`.length
);
// Indentation should not be preserved when removing lists
toggleList(ListType.OrderedList)(editor);
expect(editor.state.selection.main.from).toBe(preSubListText.length);
expect(editor.state.doc.toString()).toBe(
'# List test\n * This\n * is\na\ntest\n * of list toggling'
);
// Put the cursor in the middle of the list
editor.dispatch({ selection: EditorSelection.cursor(preSubListText.length) });
// Sublists should be changed
toggleList(ListType.CheckList)(editor);
const expectedChecklistPart =
'# List test\n - [ ] This\n - [ ] is\n - [ ] a\n - [ ] test\n - [ ] of list toggling';
expect(editor.state.doc.toString()).toBe(
expectedChecklistPart
);
editor.dispatch({ selection: EditorSelection.cursor(editor.state.doc.length) });
editor.dispatch(editor.state.replaceSelection('\n\n\n'));
// toggleList should also create a new list if the cursor is on an empty line.
toggleList(ListType.OrderedList)(editor);
editor.dispatch(editor.state.replaceSelection('Test.\n2. Test2\n3. Test3'));
expect(editor.state.doc.toString()).toBe(
`${expectedChecklistPart}\n\n\n1. Test.\n2. Test2\n3. Test3`
);
toggleList(ListType.CheckList)(editor);
expect(editor.state.doc.toString()).toBe(
`${expectedChecklistPart}\n\n\n- [ ] Test.\n- [ ] Test2\n- [ ] Test3`
);
// The entire checklist should have been selected (and thus will now be indented)
increaseIndent(editor);
expect(editor.state.doc.toString()).toBe(
`${expectedChecklistPart}\n\n\n\t- [ ] Test.\n\t- [ ] Test2\n\t- [ ] Test3`
);
});
it('should toggle a numbered list without changing its sublists', () => {
const initialDocText = '1. Foo\n2. Bar\n3. Baz\n\t- Test\n\t- of\n\t- sublists\n4. Foo';
const editor = createEditor(
initialDocText,
EditorSelection.cursor(0)
);
toggleList(ListType.CheckList)(editor);
expect(editor.state.doc.toString()).toBe(
'- [ ] Foo\n- [ ] Bar\n- [ ] Baz\n\t- Test\n\t- of\n\t- sublists\n- [ ] Foo'
);
});
it('should toggle a sublist without changing the parent list', () => {
const initialDocText = '1. This\n2. is\n3. ';
const editor = createEditor(
initialDocText,
EditorSelection.cursor(initialDocText.length)
);
increaseIndent(editor);
expect(editor.state.selection.main.empty).toBe(true);
toggleList(ListType.CheckList)(editor);
expect(editor.state.doc.toString()).toBe(
'1. This\n2. is\n\t- [ ] '
);
editor.dispatch(editor.state.replaceSelection('a test.'));
expect(editor.state.doc.toString()).toBe(
'1. This\n2. is\n\t- [ ] a test.'
);
});
it('should toggle lists properly within block quotes', () => {
const preSubListText = '> # List test\n> * This\n> * is\n';
const initialDocText = `${preSubListText}> \t* a\n> \t* test\n> * of list toggling`;
const editor = createEditor(
initialDocText, EditorSelection.cursor(preSubListText.length + 3)
);
toggleList(ListType.OrderedList)(editor);
expect(editor.state.doc.toString()).toBe(
'> # List test\n> * This\n> * is\n> \t1. a\n> \t2. test\n> * of list toggling'
);
expect(editor.state.selection.main.from).toBe(preSubListText.length);
});
});

View File

@@ -1,440 +0,0 @@
// CodeMirror 6 commands that modify markdown formatting (e.g. toggleBold).
import { EditorView, Command } from '@codemirror/view';
import { ListType } from '../types';
import {
SelectionRange, EditorSelection, ChangeSpec, Line, TransactionSpec,
} from '@codemirror/state';
import { getIndentUnit, indentString, syntaxTree } from '@codemirror/language';
import {
RegionSpec, growSelectionToNode, renumberList,
toggleInlineFormatGlobally, toggleRegionFormatGlobally, toggleSelectedLinesStartWith,
isIndentationEquivalent, stripBlockquote, tabsToSpaces,
} from './markdownReformatter';
const startingSpaceRegex = /^(\s*)/;
export const toggleBolded: Command = (view: EditorView): boolean => {
const spec = RegionSpec.of({ template: '**', nodeName: 'StrongEmphasis' });
const changes = toggleInlineFormatGlobally(view.state, spec);
view.dispatch(changes);
return true;
};
export const toggleItalicized: Command = (view: EditorView): boolean => {
const changes = toggleInlineFormatGlobally(view.state, {
nodeName: 'Emphasis',
template: { start: '*', end: '*' },
matcher: { start: /[_*]/g, end: /[_*]/g },
});
view.dispatch(changes);
return true;
};
// If the selected region is an empty inline code block, it will be converted to
// a block (fenced) code block.
export const toggleCode: Command = (view: EditorView): boolean => {
const codeFenceRegex = /^```\w*\s*$/;
const inlineRegionSpec = RegionSpec.of({ template: '`', nodeName: 'InlineCode' });
const blockRegionSpec: RegionSpec = {
nodeName: 'FencedCode',
template: { start: '```', end: '```' },
matcher: { start: codeFenceRegex, end: codeFenceRegex },
};
const changes = toggleRegionFormatGlobally(view.state, inlineRegionSpec, blockRegionSpec);
view.dispatch(changes);
return true;
};
export const toggleMath: Command = (view: EditorView): boolean => {
const blockStartRegex = /^\$\$/;
const blockEndRegex = /\$\$\s*$/;
const inlineRegionSpec = RegionSpec.of({ nodeName: 'InlineMath', template: '$' });
const blockRegionSpec = RegionSpec.of({
nodeName: 'BlockMath',
template: '$$',
matcher: {
start: blockStartRegex,
end: blockEndRegex,
},
});
const changes = toggleRegionFormatGlobally(view.state, inlineRegionSpec, blockRegionSpec);
view.dispatch(changes);
return true;
};
export const toggleList = (listType: ListType): Command => {
return (view: EditorView): boolean => {
let state = view.state;
let doc = state.doc;
const orderedListTag = 'OrderedList';
const unorderedListTag = 'BulletList';
// RegExps for different list types. The regular expressions MUST
// be mutually exclusive.
// `(?!\[[ xX]+\]\s?)` means "not followed by [x] or [ ]".
const bulletedRegex = /^\s*([-*])(?!\s\[[ xX]+\])\s?/;
const checklistRegex = /^\s*[-*]\s\[[ xX]+\]\s?/;
const numberedRegex = /^\s*\d+\.\s?/;
const listRegexes: Record<ListType, RegExp> = {
[ListType.OrderedList]: numberedRegex,
[ListType.CheckList]: checklistRegex,
[ListType.UnorderedList]: bulletedRegex,
};
const getContainerType = (line: Line): ListType|null => {
const lineContent = stripBlockquote(line);
// Determine the container's type.
const checklistMatch = lineContent.match(checklistRegex);
const bulletListMatch = lineContent.match(bulletedRegex);
const orderedListMatch = lineContent.match(numberedRegex);
if (checklistMatch) {
return ListType.CheckList;
} else if (bulletListMatch) {
return ListType.UnorderedList;
} else if (orderedListMatch) {
return ListType.OrderedList;
}
return null;
};
const changes: TransactionSpec = state.changeByRange((sel: SelectionRange) => {
const changes: ChangeSpec[] = [];
let containerType: ListType|null = null;
// Total number of characters added (deleted if negative)
let charsAdded = 0;
const originalSel = sel;
let fromLine: Line;
let toLine: Line;
let firstLineIndentation: string;
let firstLineInBlockQuote: boolean;
let fromLineContent: string;
const computeSelectionProps = () => {
fromLine = doc.lineAt(sel.from);
toLine = doc.lineAt(sel.to);
fromLineContent = stripBlockquote(fromLine);
firstLineIndentation = fromLineContent.match(startingSpaceRegex)[0];
firstLineInBlockQuote = (fromLineContent !== fromLine.text);
containerType = getContainerType(fromLine);
};
computeSelectionProps();
const origFirstLineIndentation = firstLineIndentation;
const origContainerType = containerType;
// Grow [sel] to the smallest containing list
if (sel.empty) {
sel = growSelectionToNode(state, sel, [orderedListTag, unorderedListTag]);
computeSelectionProps();
}
// Reset the selection if it seems likely the user didn't want the selection
// to be expanded
const isIndentationDiff =
!isIndentationEquivalent(state, firstLineIndentation, origFirstLineIndentation);
if (isIndentationDiff) {
const expandedRegionIndentation = firstLineIndentation;
sel = originalSel;
computeSelectionProps();
// Use the indentation level of the expanded region if it's greater.
// This makes sense in the case where unindented text is being converted to
// the same type of list as its container. For example,
// 1. Foobar
// unindented text
// that should be made a part of the above list.
//
// becoming
//
// 1. Foobar
// 2. unindented text
// 3. that should be made a part of the above list.
const wasGreaterIndentation = (
tabsToSpaces(state, expandedRegionIndentation).length
> tabsToSpaces(state, firstLineIndentation).length
);
if (wasGreaterIndentation) {
firstLineIndentation = expandedRegionIndentation;
}
} else if (
(origContainerType !== containerType && (origContainerType ?? null) !== null)
|| containerType !== getContainerType(toLine)
) {
// If the container type changed, this could be an artifact of checklists/bulleted
// lists sharing the same node type.
// Find the closest range of the same type of list to the original selection
let newFromLineNo = doc.lineAt(originalSel.from).number;
let newToLineNo = doc.lineAt(originalSel.to).number;
let lastFromLineNo;
let lastToLineNo;
while (newFromLineNo !== lastFromLineNo || newToLineNo !== lastToLineNo) {
lastFromLineNo = newFromLineNo;
lastToLineNo = newToLineNo;
if (lastFromLineNo - 1 >= 1) {
const testFromLine = doc.line(lastFromLineNo - 1);
if (getContainerType(testFromLine) === origContainerType) {
newFromLineNo --;
}
}
if (lastToLineNo + 1 <= doc.lines) {
const testToLine = doc.line(lastToLineNo + 1);
if (getContainerType(testToLine) === origContainerType) {
newToLineNo ++;
}
}
}
sel = EditorSelection.range(
doc.line(newFromLineNo).from,
doc.line(newToLineNo).to
);
computeSelectionProps();
}
// Determine whether the expanded selection should be empty
if (originalSel.empty && fromLine.number === toLine.number) {
sel = EditorSelection.cursor(toLine.to);
}
// Select entire lines (if not just a cursor)
if (!sel.empty) {
sel = EditorSelection.range(fromLine.from, toLine.to);
}
// Number of the item in the list (e.g. 2 for the 2nd item in the list)
let listItemCounter = 1;
for (let lineNum = fromLine.number; lineNum <= toLine.number; lineNum ++) {
const line = doc.line(lineNum);
const lineContent = stripBlockquote(line);
const lineContentFrom = line.to - lineContent.length;
const inBlockQuote = (lineContent !== line.text);
const indentation = lineContent.match(startingSpaceRegex)[0];
const wrongIndentaton = !isIndentationEquivalent(state, indentation, firstLineIndentation);
// If not the right list level,
if (inBlockQuote !== firstLineInBlockQuote || wrongIndentaton) {
// We'll be starting a new list
listItemCounter = 1;
continue;
}
// Don't add list numbers to otherwise empty lines (unless it's the first line)
if (lineNum !== fromLine.number && line.text.trim().length === 0) {
// Do not reset the counter -- the markdown renderer doesn't!
continue;
}
const deleteFrom = lineContentFrom;
let deleteTo = deleteFrom + indentation.length;
// If we need to remove an existing list,
const currentContainer = getContainerType(line);
if (currentContainer !== null) {
const containerRegex = listRegexes[currentContainer];
const containerMatch = lineContent.match(containerRegex);
if (!containerMatch) {
throw new Error(
'Assertion failed: container regex does not match line content.'
);
}
deleteTo = lineContentFrom + containerMatch[0].length;
}
let replacementString;
if (listType === containerType) {
// Delete the existing list if it's the same type as the current
replacementString = '';
} else if (listType === ListType.OrderedList) {
replacementString = `${firstLineIndentation}${listItemCounter}. `;
} else if (listType === ListType.CheckList) {
replacementString = `${firstLineIndentation}- [ ] `;
} else {
replacementString = `${firstLineIndentation}- `;
}
changes.push({
from: deleteFrom,
to: deleteTo,
insert: replacementString,
});
charsAdded -= deleteTo - deleteFrom;
charsAdded += replacementString.length;
listItemCounter++;
}
// Don't change cursors to selections
if (sel.empty) {
// Position the cursor at the end of the last line modified
sel = EditorSelection.cursor(toLine.to + charsAdded);
} else {
sel = EditorSelection.range(
sel.from,
sel.to + charsAdded
);
}
return {
changes,
range: sel,
};
});
view.dispatch(changes);
state = view.state;
doc = state.doc;
// Renumber the list
view.dispatch(state.changeByRange((sel: SelectionRange) => {
return renumberList(state, sel);
}));
return true;
};
};
export const toggleHeaderLevel = (level: number): Command => {
return (view: EditorView): boolean => {
let headerStr = '';
for (let i = 0; i < level; i++) {
headerStr += '#';
}
const matchEmpty = true;
// Remove header formatting for any other level
let changes = toggleSelectedLinesStartWith(
view.state,
new RegExp(
// Check all numbers of #s lower than [level]
`${level - 1 >= 1 ? `(?:^[#]{1,${level - 1}}\\s)|` : ''
// Check all number of #s higher than [level]
}(?:^[#]{${level + 1},}\\s)`
),
'',
matchEmpty
);
view.dispatch(changes);
// Set to the proper header level
changes = toggleSelectedLinesStartWith(
view.state,
// We want exactly [level] '#' characters.
new RegExp(`^[#]{${level}} `),
`${headerStr} `,
matchEmpty
);
view.dispatch(changes);
return true;
};
};
// Prepends the given editor's indentUnit to all lines of the current selection
// and re-numbers modified ordered lists (if any).
export const increaseIndent: Command = (view: EditorView): boolean => {
const matchEmpty = true;
const matchNothing = /$ ^/;
const indentUnit = indentString(view.state, getIndentUnit(view.state));
const changes = toggleSelectedLinesStartWith(
view.state,
// Delete nothing
matchNothing,
// ...and thus always add indentUnit.
indentUnit,
matchEmpty
);
view.dispatch(changes);
// Fix any lists
view.dispatch(view.state.changeByRange((sel: SelectionRange) => {
return renumberList(view.state, sel);
}));
return true;
};
export const decreaseIndent: Command = (view: EditorView): boolean => {
const matchEmpty = true;
const changes = toggleSelectedLinesStartWith(
view.state,
// Assume indentation is either a tab or in units
// of n spaces.
new RegExp(`^(?:[\\t]|[ ]{1,${getIndentUnit(view.state)}})`),
// Don't add new text
'',
matchEmpty
);
view.dispatch(changes);
// Fix any lists
view.dispatch(view.state.changeByRange((sel: SelectionRange) => {
return renumberList(view.state, sel);
}));
return true;
};
export const updateLink = (label: string, url: string): Command => {
// Empty label? Just include the URL.
const linkText = label === '' ? url : `[${label}](${url})`;
return (editor: EditorView): boolean => {
const transaction = editor.state.changeByRange((sel: SelectionRange) => {
const changes = [];
// Search for a link that overlaps [sel]
let linkFrom: number | null = null;
let linkTo: number | null = null;
syntaxTree(editor.state).iterate({
from: sel.from, to: sel.to,
enter: node => {
const haveFoundLink = (linkFrom !== null && linkTo !== null);
if (node.name === 'Link' || (node.name === 'URL' && !haveFoundLink)) {
linkFrom = node.from;
linkTo = node.to;
}
},
});
linkFrom ??= sel.from;
linkTo ??= sel.to;
changes.push({
from: linkFrom, to: linkTo,
insert: linkText,
});
return {
changes,
range: EditorSelection.range(linkFrom, linkFrom + linkText.length),
};
});
editor.dispatch(transaction);
return true;
};
};

View File

@@ -1,152 +0,0 @@
import { markdown } from '@codemirror/lang-markdown';
import { ensureSyntaxTree } from '@codemirror/language';
import { SyntaxNode } from '@lezer/common';
import { EditorState } from '@codemirror/state';
import { blockMathTagName, inlineMathContentTagName, inlineMathTagName, MarkdownMathExtension } from './markdownMathParser';
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
const syntaxTreeCreateTimeout = 100; // ms
/** Create an EditorState with markdown extensions */
const createEditorState = (initialText: string): EditorState => {
return EditorState.create({
doc: initialText,
extensions: [
markdown({
extensions: [MarkdownMathExtension, GithubFlavoredMarkdownExt],
}),
],
});
};
/**
* Returns a list of all nodes with the given name in the given editor's syntax tree.
* Attempts to create the syntax tree if it doesn't exist.
*/
const findNodesWithName = (editor: EditorState, nodeName: string) => {
const result: SyntaxNode[] = [];
ensureSyntaxTree(editor, syntaxTreeCreateTimeout)?.iterate({
enter: (node) => {
if (node.name === nodeName) {
result.push(node.node);
}
},
});
return result;
};
describe('Inline parsing', () => {
it('Document with just a math region', () => {
const documentText = '$3 + 3$';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const inlineMathContentNodes = findNodesWithName(editor, inlineMathContentTagName);
// There should only be one inline node
expect(inlineMathNodes.length).toBe(1);
expect(inlineMathNodes[0].from).toBe(0);
expect(inlineMathNodes[0].to).toBe(documentText.length);
// The content tag should be replaced by the internal sTeX parser
expect(inlineMathContentNodes.length).toBe(0);
});
it('Inline math mixed with text', () => {
const beforeMath = '# Testing!\n\nThis is a test of ';
const mathRegion = '$\\TeX % TeX Comment!$';
const afterMath = ' formatting.';
const documentText = `${beforeMath}${mathRegion}${afterMath}`;
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
const commentNodes = findNodesWithName(editor, 'comment');
expect(inlineMathNodes.length).toBe(1);
expect(blockMathNodes.length).toBe(0);
expect(commentNodes.length).toBe(1);
expect(inlineMathNodes[0].from).toBe(beforeMath.length);
expect(inlineMathNodes[0].to).toBe(beforeMath.length + mathRegion.length);
});
it('Inline math with no ending $ in a block', () => {
const documentText = 'This is a $test\n\nof inline math$...';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
// Math should end if there is no matching '$'.
expect(inlineMathNodes.length).toBe(0);
});
it('Shouldn\'t start if block would have spaces just inside', () => {
const documentText = 'This is a $ test of inline math$...\n\n$Testing... $...';
const editor = createEditorState(documentText);
expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0);
});
it('Shouldn\'t start if $ is escaped', () => {
const documentText = 'This is a \\$test of inline math$...';
const editor = createEditorState(documentText);
expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0);
});
});
describe('Block math tests', () => {
it('Document with just block math', () => {
const documentText = '$$\n\t\\{ 1, 1, 2, 3, 5, ... \\}\n$$';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(inlineMathNodes.length).toBe(0);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
it('Block math with comment', () => {
const startingText = '$$ % Testing...\n\t\\text{Test.}\n$$';
const afterMath = '\nTest.';
const editor = createEditorState(startingText + afterMath);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
const texParserComments = findNodesWithName(editor, 'comment');
expect(inlineMathNodes.length).toBe(0);
expect(blockMathNodes.length).toBe(1);
expect(texParserComments.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(startingText.length);
expect(texParserComments[0]).toMatchObject({
from: '$$ '.length,
to: '$$ % Testing...'.length,
});
});
it('Block math without an ending tag', () => {
const beforeMath = '# Testing...\n\n';
const documentText = `${beforeMath}$$\n\t\\text{Testing...}\n\n\t3 + 3 = 6`;
const editor = createEditorState(documentText);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(beforeMath.length);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
it('Single-line declaration of block math', () => {
const documentText = '$$ Test. $$';
const editor = createEditorState(documentText);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
});

View File

@@ -1,216 +0,0 @@
/**
* Search for $s and $$s in markdown and mark the regions between them as math.
*
* Text between single $s is marked as InlineMath and text between $$s is marked
* as BlockMath.
*/
import { tags, Tag } from '@lezer/highlight';
import { parseMixed, SyntaxNodeRef, Input, NestedParse, ParseWrapper } from '@lezer/common';
// Extend the existing markdown parser
import {
MarkdownConfig, InlineContext,
BlockContext, Line, LeafBlock,
} from '@lezer/markdown';
// The existing stexMath parser is used to parse the text between the $s
import { stexMath } from '@codemirror/legacy-modes/mode/stex';
import { StreamLanguage } from '@codemirror/language';
const dollarSignCharcode = 36;
const backslashCharcode = 92;
// (?:[>]\s*)?: Optionally allow block math lines to start with '> '
const mathBlockStartRegex = /^(?:\s*[>]\s*)?\$\$/;
const mathBlockEndRegex = /\$\$\s*$/;
const texLanguage = StreamLanguage.define(stexMath);
export const blockMathTagName = 'BlockMath';
export const blockMathContentTagName = 'BlockMathContent';
export const inlineMathTagName = 'InlineMath';
export const inlineMathContentTagName = 'InlineMathContent';
export const mathTag = Tag.define(tags.monospace);
export const inlineMathTag = Tag.define(mathTag);
/**
* Wraps a TeX math-mode parser. This removes [nodeTag] from the syntax tree
* and replaces it with a region handled by the sTeXMath parser.
*
* @param nodeTag Name of the nodes to replace with regions parsed by the sTeX parser.
* @returns a wrapped sTeX parser.
*/
const wrappedTeXParser = (nodeTag: string): ParseWrapper => {
return parseMixed((node: SyntaxNodeRef, _input: Input): NestedParse => {
if (node.name !== nodeTag) {
return null;
}
return {
parser: texLanguage.parser,
};
});
};
// Markdown extension for recognizing inline code
const InlineMathConfig: MarkdownConfig = {
defineNodes: [
{
name: inlineMathTagName,
style: inlineMathTag,
},
{
name: inlineMathContentTagName,
},
],
parseInline: [{
name: inlineMathTagName,
after: 'InlineCode',
parse(cx: InlineContext, current: number, pos: number): number {
const prevCharCode = pos - 1 >= 0 ? cx.char(pos - 1) : -1;
const nextCharCode = cx.char(pos + 1);
if (current !== dollarSignCharcode
|| prevCharCode === dollarSignCharcode
|| nextCharCode === dollarSignCharcode) {
return -1;
}
// Don't match if there's a space directly after the '$'
if (/\s/.exec(String.fromCharCode(nextCharCode))) {
return -1;
}
const start = pos;
const end = cx.end;
let escaped = false;
pos ++;
// Scan ahead for the next '$' symbol
for (; pos < end && (escaped || cx.char(pos) !== dollarSignCharcode); pos++) {
if (!escaped && cx.char(pos) === backslashCharcode) {
escaped = true;
} else {
escaped = false;
}
}
// Don't match if the ending '$' is preceded by a space.
const prevChar = String.fromCharCode(cx.char(pos - 1));
if (/\s/.exec(prevChar)) {
return -1;
}
// It isn't a math region if there is no ending '$'
if (pos === end) {
return -1;
}
// Advance to just after the ending '$'
pos ++;
// Add a wraping inlineMathTagName node that contains an inlineMathContentTagName.
// The inlineMathContentTagName node can thus be safely removed and the region
// will still be marked as a math region.
const contentElem = cx.elt(inlineMathContentTagName, start + 1, pos - 1);
cx.addElement(cx.elt(inlineMathTagName, start, pos, [contentElem]));
return pos + 1;
},
}],
wrap: wrappedTeXParser(inlineMathContentTagName),
};
// Extension for recognising block code
const BlockMathConfig: MarkdownConfig = {
defineNodes: [
{
name: blockMathTagName,
style: mathTag,
},
{
name: blockMathContentTagName,
},
],
parseBlock: [{
name: blockMathTagName,
before: 'Blockquote',
parse(cx: BlockContext, line: Line): boolean {
const delimLen = 2;
// $$ delimiter? Start math!
const mathStartMatch = mathBlockStartRegex.exec(line.text);
if (mathStartMatch) {
const start = cx.lineStart + mathStartMatch[0].length;
let stop;
let endMatch = mathBlockEndRegex.exec(
line.text.substring(mathStartMatch[0].length)
);
// If the math region ends immediately (on the same line),
if (endMatch) {
const lineLength = line.text.length;
stop = cx.lineStart + lineLength - endMatch[0].length;
} else {
let hadNextLine = false;
// Otherwise, it's a multi-line block display.
// Consume lines until we reach the end.
do {
hadNextLine = cx.nextLine();
endMatch = hadNextLine ? mathBlockEndRegex.exec(line.text) : null;
}
while (hadNextLine && endMatch === null);
if (hadNextLine && endMatch) {
const lineLength = line.text.length;
// Remove the ending delimiter
stop = cx.lineStart + lineLength - endMatch[0].length;
} else {
stop = cx.lineStart;
}
}
const lineEnd = cx.lineStart + line.text.length;
// Label the region. Add two labels so that one can be removed.
const contentElem = cx.elt(blockMathContentTagName, start, stop);
const containerElement = cx.elt(
blockMathTagName,
start - delimLen,
// Math blocks don't need ending delimiters, so ensure we don't
// include text that doesn't exist.
Math.min(lineEnd, stop + delimLen),
// The child of the container element should be the content element
[contentElem]
);
cx.addElement(containerElement);
// Don't re-process the ending delimiter (it may look the same
// as the starting delimiter).
cx.nextLine();
return true;
}
return false;
},
// End paragraph-like blocks
endLeaf(_cx: BlockContext, line: Line, _leaf: LeafBlock): boolean {
// Leaf blocks (e.g. block quotes) end early if math starts.
return mathBlockStartRegex.exec(line.text) !== null;
},
}],
wrap: wrappedTeXParser(blockMathContentTagName),
};
/** Markdown configuration for block and inline math support. */
export const MarkdownMathExtension: MarkdownConfig[] = [
InlineMathConfig,
BlockMathConfig,
];

View File

@@ -1,142 +0,0 @@
import {
findInlineMatch, MatchSide, RegionSpec, tabsToSpaces, toggleRegionFormatGlobally,
} from './markdownReformatter';
import { Text as DocumentText, EditorSelection, EditorState } from '@codemirror/state';
import { indentUnit } from '@codemirror/language';
describe('markdownReformatter', () => {
const boldSpec: RegionSpec = RegionSpec.of({
template: '**',
});
it('matching a bolded region: should return the length of the match', () => {
const doc = DocumentText.of(['**test**']);
const sel = EditorSelection.range(0, 5);
// matchStart returns the length of the match
expect(findInlineMatch(doc, boldSpec, sel, MatchSide.Start)).toBe(2);
});
it('matching a bolded region: should match the end of a region, if next to the cursor', () => {
const doc = DocumentText.of(['**...** test.']);
const sel = EditorSelection.range(5, 5);
expect(findInlineMatch(doc, boldSpec, sel, MatchSide.End)).toBe(2);
});
it('matching a bolded region: should return -1 if no match is found', () => {
const doc = DocumentText.of(['**...** test.']);
const sel = EditorSelection.range(3, 3);
expect(findInlineMatch(doc, boldSpec, sel, MatchSide.Start)).toBe(-1);
});
it('should match a custom specification of italicized regions', () => {
const spec: RegionSpec = {
template: { start: '*', end: '*' },
matcher: { start: /[*_]/g, end: /[*_]/g },
};
const testString = 'This is a _test_';
const testDoc = DocumentText.of([testString]);
const fullSel = EditorSelection.range('This is a '.length, testString.length);
// should match the start of the region
expect(findInlineMatch(testDoc, spec, fullSel, MatchSide.Start)).toBe(1);
// should match the end of the region
expect(findInlineMatch(testDoc, spec, fullSel, MatchSide.End)).toBe(1);
});
const listSpec: RegionSpec = {
template: { start: ' - ', end: '' },
matcher: {
start: /^\s*[-*]\s/g,
end: /$/g,
},
};
it('matching a custom list: should not match a list if not within the selection', () => {
const doc = DocumentText.of(['- Test...']);
const sel = EditorSelection.range(1, 6);
// Beginning of list not selected: no match
expect(findInlineMatch(doc, listSpec, sel, MatchSide.Start)).toBe(-1);
});
it('matching a custom list: should match start of selected, unindented list', () => {
const doc = DocumentText.of(['- Test...']);
const sel = EditorSelection.range(0, 6);
expect(findInlineMatch(doc, listSpec, sel, MatchSide.Start)).toBe(2);
});
it('matching a custom list: should match start of indented list', () => {
const doc = DocumentText.of([' - Test...']);
const sel = EditorSelection.range(0, 6);
expect(findInlineMatch(doc, listSpec, sel, MatchSide.Start)).toBe(5);
});
it('matching a custom list: should match the end of an item in an indented list', () => {
const doc = DocumentText.of([' - Test...']);
const sel = EditorSelection.range(0, 6);
// Zero-length, but found, selection
expect(findInlineMatch(doc, listSpec, sel, MatchSide.End)).toBe(0);
});
const multiLineTestText = `Internal text manipulation
This is a test...
of block and inline region toggling.`;
const codeFenceRegex = /^``````\w*\s*$/;
const inlineCodeRegionSpec = RegionSpec.of({
template: '`',
nodeName: 'InlineCode',
});
const blockCodeRegionSpec: RegionSpec = {
template: { start: '``````', end: '``````' },
matcher: { start: codeFenceRegex, end: codeFenceRegex },
};
it('should create an empty inline region around the cursor, if given an empty selection', () => {
const initialState: EditorState = EditorState.create({
doc: multiLineTestText,
selection: EditorSelection.cursor(0),
});
const changes = toggleRegionFormatGlobally(
initialState, inlineCodeRegionSpec, blockCodeRegionSpec
);
const newState = initialState.update(changes).state;
expect(newState.doc.toString()).toEqual(`\`\`${multiLineTestText}`);
});
it('should wrap multiple selected lines in block formatting', () => {
const initialState: EditorState = EditorState.create({
doc: multiLineTestText,
selection: EditorSelection.range(0, multiLineTestText.length),
});
const changes = toggleRegionFormatGlobally(
initialState, inlineCodeRegionSpec, blockCodeRegionSpec
);
const newState = initialState.update(changes).state;
const editorText = newState.doc.toString();
expect(editorText).toBe(`\`\`\`\`\`\`\n${multiLineTestText}\n\`\`\`\`\`\``);
expect(newState.selection.main.from).toBe(0);
expect(newState.selection.main.to).toBe(editorText.length);
});
it('should convert tabs to spaces based on indentUnit', () => {
const state: EditorState = EditorState.create({
doc: multiLineTestText,
selection: EditorSelection.cursor(0),
extensions: [
indentUnit.of(' '),
],
});
expect(tabsToSpaces(state, '\t')).toBe(' ');
expect(tabsToSpaces(state, '\t ')).toBe(' ');
expect(tabsToSpaces(state, ' \t ')).toBe(' ');
});
});

View File

@@ -1,712 +0,0 @@
import {
Text as DocumentText, EditorSelection, SelectionRange, ChangeSpec, EditorState, Line, TransactionSpec,
} from '@codemirror/state';
import { getIndentUnit, syntaxTree } from '@codemirror/language';
import { SyntaxNodeRef } from '@lezer/common';
// pregQuote escapes text for usage in regular expressions
const { pregQuote } = require('@joplin/lib/string-utils-common');
// Length of the symbol that starts a block quote
const blockQuoteStartLen = '> '.length;
const blockQuoteRegex = /^>\s/;
// Specifies the update of a single selection region and its contents
type SelectionUpdate = { range: SelectionRange; changes?: ChangeSpec };
// Specifies how a to find the start/stop of a type of formatting
interface RegionMatchSpec {
start: RegExp;
end: RegExp;
}
// Describes a region's formatting
export interface RegionSpec {
// The name of the node corresponding to the region in the syntax tree
nodeName?: string;
// Text to be inserted before and after the region when toggling.
template: { start: string; end: string };
// How to identify the region
matcher: RegionMatchSpec;
}
export namespace RegionSpec { // eslint-disable-line no-redeclare
interface RegionSpecConfig {
nodeName?: string;
template: string | { start: string; end: string };
matcher?: RegionMatchSpec;
}
// Creates a new RegionSpec, given a simplified set of options.
// If [config.template] is a string, it is used as both the starting and ending
// templates.
// Similarly, if [config.matcher] is not given, a matcher is created based on
// [config.template].
export const of = (config: RegionSpecConfig): RegionSpec => {
let templateStart: string, templateEnd: string;
if (typeof config.template === 'string') {
templateStart = config.template;
templateEnd = config.template;
} else {
templateStart = config.template.start;
templateEnd = config.template.end;
}
const matcher: RegionMatchSpec =
config.matcher ?? matcherFromTemplate(templateStart, templateEnd);
return {
nodeName: config.nodeName,
template: { start: templateStart, end: templateEnd },
matcher,
};
};
const matcherFromTemplate = (start: string, end: string): RegionMatchSpec => {
// See https://stackoverflow.com/a/30851002
const escapedStart = pregQuote(start);
const escapedEnd = pregQuote(end);
return {
start: new RegExp(escapedStart, 'g'),
end: new RegExp(escapedEnd, 'g'),
};
};
}
export enum MatchSide {
Start,
End,
}
// Returns the length of a match for this in the given selection,
// -1 if no match is found.
export const findInlineMatch = (
doc: DocumentText, spec: RegionSpec, sel: SelectionRange, side: MatchSide
): number => {
const [regex, template] = (() => {
if (side === MatchSide.Start) {
return [spec.matcher.start, spec.template.start];
} else {
return [spec.matcher.end, spec.template.end];
}
})();
const [startIndex, endIndex] = (() => {
if (!sel.empty) {
return [sel.from, sel.to];
}
const bufferSize = template.length;
if (side === MatchSide.Start) {
return [sel.from - bufferSize, sel.to];
} else {
return [sel.from, sel.to + bufferSize];
}
})();
const searchText = doc.sliceString(startIndex, endIndex);
// Returns true if [idx] is in the right place (the match is at
// the end of the string or the beginning based on startIndex/endIndex).
const indexSatisfies = (idx: number, len: number): boolean => {
idx += startIndex;
if (side === MatchSide.Start) {
return idx === startIndex;
} else {
return idx + len === endIndex;
}
};
// Enforce 'g' flag.
if (!regex.global) {
throw new Error('Regular expressions used by RegionSpec must have the global flag!');
}
// Search from the beginning.
regex.lastIndex = 0;
let foundMatch = null;
let match;
while ((match = regex.exec(searchText)) !== null) {
if (indexSatisfies(match.index, match[0].length)) {
foundMatch = match;
break;
}
}
if (foundMatch) {
const matchLength = foundMatch[0].length;
const matchIndex = foundMatch.index;
// If the match isn't in the right place,
if (indexSatisfies(matchIndex, matchLength)) {
return matchLength;
}
}
return -1;
};
export const stripBlockquote = (line: Line): string => {
const match = line.text.match(blockQuoteRegex);
if (match) {
return line.text.substring(match[0].length);
}
return line.text;
};
export const tabsToSpaces = (state: EditorState, text: string): string => {
const chunks = text.split('\t');
const spaceLen = getIndentUnit(state);
let result = chunks[0];
for (let i = 1; i < chunks.length; i++) {
for (let j = result.length % spaceLen; j < spaceLen; j++) {
result += ' ';
}
result += chunks[i];
}
return result;
};
// Returns true iff [a] (an indentation string) is roughly equivalent to [b].
export const isIndentationEquivalent = (state: EditorState, a: string, b: string): boolean => {
// Consider sublists to be the same as their parent list if they have the same
// label plus or minus 1 space.
return Math.abs(tabsToSpaces(state, a).length - tabsToSpaces(state, b).length) <= 1;
};
// Expands and returns a copy of [sel] to the smallest container node with name in [nodeNames].
export const growSelectionToNode = (
state: EditorState, sel: SelectionRange, nodeNames: string|string[]|null
): SelectionRange => {
if (!nodeNames) {
return sel;
}
const isAcceptableNode = (name: string): boolean => {
if (typeof nodeNames === 'string') {
return name === nodeNames;
}
for (const otherName of nodeNames) {
if (otherName === name) {
return true;
}
}
return false;
};
let newFrom = null;
let newTo = null;
let smallestLen = Infinity;
// Find the smallest range.
syntaxTree(state).iterate({
from: sel.from, to: sel.to,
enter: node => {
if (isAcceptableNode(node.name)) {
if (node.to - node.from < smallestLen) {
newFrom = node.from;
newTo = node.to;
smallestLen = newTo - newFrom;
}
}
},
});
// If it's in such a node,
if (newFrom !== null && newTo !== null) {
return EditorSelection.range(newFrom, newTo);
} else {
return sel;
}
};
// Toggles whether the given selection matches the inline region specified by [spec].
//
// For example, something similar to toggleSurrounded('**', '**') would surround
// every selection range with asterisks (including the caret).
// If the selection is already surrounded by these characters, they are
// removed.
const toggleInlineRegionSurrounded = (
doc: DocumentText, sel: SelectionRange, spec: RegionSpec
): SelectionUpdate => {
let content = doc.sliceString(sel.from, sel.to);
const startMatchLen = findInlineMatch(doc, spec, sel, MatchSide.Start);
const endMatchLen = findInlineMatch(doc, spec, sel, MatchSide.End);
const startsWithBefore = startMatchLen >= 0;
const endsWithAfter = endMatchLen >= 0;
const changes = [];
let finalSelStart = sel.from;
let finalSelEnd = sel.to;
if (startsWithBefore && endsWithAfter) {
// Remove the before and after.
content = content.substring(startMatchLen);
content = content.substring(0, content.length - endMatchLen);
finalSelEnd -= startMatchLen + endMatchLen;
changes.push({
from: sel.from,
to: sel.to,
insert: content,
});
} else {
changes.push({
from: sel.from,
insert: spec.template.start,
});
changes.push({
from: sel.to,
insert: spec.template.start,
});
// If not a caret,
if (!sel.empty) {
// Select the surrounding chars.
finalSelEnd += spec.template.start.length + spec.template.end.length;
} else {
// Position the caret within the added content.
finalSelStart = sel.from + spec.template.start.length;
finalSelEnd = finalSelStart;
}
}
return {
changes,
range: EditorSelection.range(finalSelStart, finalSelEnd),
};
};
// Returns updated selections: For all selections in the given `EditorState`, toggles
// whether each is contained in an inline region of type [spec].
export const toggleInlineSelectionFormat = (
state: EditorState, spec: RegionSpec, sel: SelectionRange
): SelectionUpdate => {
const endMatchLen = findInlineMatch(state.doc, spec, sel, MatchSide.End);
// If at the end of the region, move the
// caret to the end.
// E.g.
// **foobar|**
// **foobar**|
if (sel.empty && endMatchLen > -1) {
const newCursorPos = sel.from + endMatchLen;
return {
range: EditorSelection.cursor(newCursorPos),
};
}
// Grow the selection to encompass the entire node.
const newRange = growSelectionToNode(state, sel, spec.nodeName);
return toggleInlineRegionSurrounded(state.doc, newRange, spec);
};
// Like toggleInlineSelectionFormat, but for all selections in [state].
export const toggleInlineFormatGlobally = (
state: EditorState, spec: RegionSpec
): TransactionSpec => {
const changes = state.changeByRange((sel: SelectionRange) => {
return toggleInlineSelectionFormat(state, spec, sel);
});
return changes;
};
// Toggle formatting in a region, applying block formatting
export const toggleRegionFormatGlobally = (
state: EditorState,
inlineSpec: RegionSpec,
blockSpec: RegionSpec
): TransactionSpec => {
const doc = state.doc;
const preserveBlockQuotes = true;
const getMatchEndPoints = (
match: RegExpMatchArray, line: Line, inBlockQuote: boolean
): [startIdx: number, stopIdx: number] => {
const startIdx = line.from + match.index;
let stopIdx;
// Don't treat '> ' as part of the line's content if we're in a blockquote.
let contentLength = line.text.length;
if (inBlockQuote && preserveBlockQuotes) {
contentLength -= blockQuoteStartLen;
}
// If it matches the entire line, remove the newline character.
if (match[0].length === contentLength) {
stopIdx = line.to + 1;
} else {
stopIdx = startIdx + match[0].length;
// Take into account the extra '> ' characters, if necessary
if (inBlockQuote && preserveBlockQuotes) {
stopIdx += blockQuoteStartLen;
}
}
stopIdx = Math.min(stopIdx, doc.length);
return [startIdx, stopIdx];
};
// Returns a change spec that converts an inline region to a block region
// only if the user's cursor is in an empty inline region.
// For example,
// $|$ -> $$\n|\n$$ where | represents the cursor.
const handleInlineToBlockConversion = (sel: SelectionRange) => {
if (!sel.empty) {
return null;
}
const startMatchLen = findInlineMatch(doc, inlineSpec, sel, MatchSide.Start);
const stopMatchLen = findInlineMatch(doc, inlineSpec, sel, MatchSide.End);
if (startMatchLen >= 0 && stopMatchLen >= 0) {
const fromLine = doc.lineAt(sel.from);
const inBlockQuote = fromLine.text.match(blockQuoteRegex);
let lineStartStr = '\n';
if (inBlockQuote && preserveBlockQuotes) {
lineStartStr = '\n> ';
}
const inlineStart = sel.from - startMatchLen;
const inlineStop = sel.from + stopMatchLen;
// Determine the text that starts the new block (e.g. \n$$\n for
// a math block).
let blockStart = `${blockSpec.template.start}${lineStartStr}`;
if (fromLine.from !== inlineStart) {
// Add a line before to put the start of the block
// on its own line.
blockStart = lineStartStr + blockStart;
}
return {
changes: [
{
from: inlineStart,
to: inlineStop,
insert: `${blockStart}${lineStartStr}${blockSpec.template.end}`,
},
],
range: EditorSelection.cursor(inlineStart + blockStart.length),
};
}
return null;
};
const changes = state.changeByRange((sel: SelectionRange) => {
const blockConversion = handleInlineToBlockConversion(sel);
if (blockConversion) {
return blockConversion;
}
// If we're in the block version, grow the selection to cover the entire region.
sel = growSelectionToNode(state, sel, blockSpec.nodeName);
const fromLine = doc.lineAt(sel.from);
const toLine = doc.lineAt(sel.to);
let fromLineText = fromLine.text;
let toLineText = toLine.text;
let charsAdded = 0;
const changes = [];
// Single line: Inline toggle.
if (fromLine.number === toLine.number) {
return toggleInlineSelectionFormat(state, inlineSpec, sel);
}
// Are all lines in a block quote?
let inBlockQuote = true;
for (let i = fromLine.number; i <= toLine.number; i++) {
const line = doc.line(i);
if (!line.text.match(blockQuoteRegex)) {
inBlockQuote = false;
break;
}
}
// Ignore block quote characters if in a block quote.
if (inBlockQuote && preserveBlockQuotes) {
fromLineText = fromLineText.substring(blockQuoteStartLen);
toLineText = toLineText.substring(blockQuoteStartLen);
}
// Otherwise, we're toggling the block version
const startMatch = blockSpec.matcher.start.exec(fromLineText);
const stopMatch = blockSpec.matcher.end.exec(toLineText);
if (startMatch && stopMatch) {
// Get start and stop indicies for the starting and ending matches
const [fromMatchFrom, fromMatchTo] = getMatchEndPoints(startMatch, fromLine, inBlockQuote);
const [toMatchFrom, toMatchTo] = getMatchEndPoints(stopMatch, toLine, inBlockQuote);
// Delete content of the first line
changes.push({
from: fromMatchFrom,
to: fromMatchTo,
});
charsAdded -= fromMatchTo - fromMatchFrom;
// Delete content of the last line
changes.push({
from: toMatchFrom,
to: toMatchTo,
});
charsAdded -= toMatchTo - toMatchFrom;
} else {
let insertBefore, insertAfter;
if (inBlockQuote && preserveBlockQuotes) {
insertBefore = `> ${blockSpec.template.start}\n`;
insertAfter = `\n> ${blockSpec.template.end}`;
} else {
insertBefore = `${blockSpec.template.start}\n`;
insertAfter = `\n${blockSpec.template.end}`;
}
changes.push({
from: fromLine.from,
insert: insertBefore,
});
changes.push({
from: toLine.to,
insert: insertAfter,
});
charsAdded += insertBefore.length + insertAfter.length;
}
return {
changes,
// Selection should now encompass all lines that were changed.
range: EditorSelection.range(
fromLine.from, toLine.to + charsAdded
),
};
});
return changes;
};
// Toggles whether all lines in the user's selection start with [regex].
export const toggleSelectedLinesStartWith = (
state: EditorState,
regex: RegExp,
template: string,
matchEmpty: boolean,
// Name associated with what [regex] matches (e.g. FencedCode)
nodeName?: string
): TransactionSpec => {
const ignoreBlockQuotes = true;
const getLineContentStart = (line: Line): number => {
if (!ignoreBlockQuotes) {
return line.from;
}
const blockQuoteMatch = line.text.match(blockQuoteRegex);
if (blockQuoteMatch) {
return line.from + blockQuoteMatch[0].length;
}
return line.from;
};
const getLineContent = (line: Line): string => {
const contentStart = getLineContentStart(line);
return line.text.substring(contentStart - line.from);
};
const changes = state.changeByRange((sel: SelectionRange) => {
// Attempt to select all lines in the region
if (nodeName && sel.empty) {
sel = growSelectionToNode(state, sel, nodeName);
}
const doc = state.doc;
const fromLine = doc.lineAt(sel.from);
const toLine = doc.lineAt(sel.to);
let hasProp = false;
let charsAdded = 0;
const changes = [];
const lines = [];
for (let i = fromLine.number; i <= toLine.number; i++) {
const line = doc.line(i);
const text = getLineContent(line);
// If already matching [regex],
if (text.search(regex) === 0) {
hasProp = true;
}
lines.push(line);
}
for (const line of lines) {
const text = getLineContent(line);
const contentFrom = getLineContentStart(line);
// Only process if the line is non-empty.
if (!matchEmpty && text.trim().length === 0
// Treat the first line differently
&& fromLine.number < line.number) {
continue;
}
if (hasProp) {
const match = text.match(regex);
if (!match) {
continue;
}
changes.push({
from: contentFrom,
to: contentFrom + match[0].length,
insert: '',
});
charsAdded -= match[0].length;
} else {
changes.push({
from: contentFrom,
insert: template,
});
charsAdded += template.length;
}
}
// If the selection is empty and a single line was changed, don't grow it.
// (user might be adding a list/header, in which case, selecting the just
// added text isn't helpful)
let newSel;
if (sel.empty && fromLine.number === toLine.number) {
const regionEnd = toLine.to + charsAdded;
newSel = EditorSelection.cursor(regionEnd);
} else {
newSel = EditorSelection.range(fromLine.from, toLine.to + charsAdded);
}
return {
changes,
// Selection should now encompass all lines that were changed.
range: newSel,
};
});
return changes;
};
// Ensures that ordered lists within [sel] are numbered in ascending order.
export const renumberList = (state: EditorState, sel: SelectionRange): SelectionUpdate => {
const doc = state.doc;
const listItemRegex = /^(\s*)(\d+)\.\s?/;
const changes: ChangeSpec[] = [];
const fromLine = doc.lineAt(sel.from);
const toLine = doc.lineAt(sel.to);
let charsAdded = 0;
// Re-numbers ordered lists and sublists with numbers on each line in [linesToHandle]
const handleLines = (linesToHandle: Line[]) => {
let currentGroupIndentation = '';
let nextListNumber = 1;
const listNumberStack: number[] = [];
let prevLineNumber;
for (const line of linesToHandle) {
// Don't re-handle lines.
if (line.number === prevLineNumber) {
continue;
}
prevLineNumber = line.number;
const filteredText = stripBlockquote(line);
const match = filteredText.match(listItemRegex);
const indentation = match[1];
const indentationLen = tabsToSpaces(state, indentation).length;
const targetIndentLen = tabsToSpaces(state, currentGroupIndentation).length;
if (targetIndentLen < indentationLen) {
listNumberStack.push(nextListNumber);
nextListNumber = 1;
} else if (targetIndentLen > indentationLen) {
nextListNumber = listNumberStack.pop() ?? parseInt(match[2], 10);
}
if (targetIndentLen !== indentationLen) {
currentGroupIndentation = indentation;
}
const from = line.to - filteredText.length;
const to = from + match[0].length;
const inserted = `${indentation}${nextListNumber}. `;
nextListNumber++;
changes.push({
from,
to,
insert: inserted,
});
charsAdded -= to - from;
charsAdded += inserted.length;
}
};
const linesToHandle: Line[] = [];
syntaxTree(state).iterate({
from: sel.from,
to: sel.to,
enter: (nodeRef: SyntaxNodeRef) => {
if (nodeRef.name === 'ListItem') {
for (const node of nodeRef.node.parent.getChildren('ListItem')) {
const line = doc.lineAt(node.from);
const filteredText = stripBlockquote(line);
const match = filteredText.match(listItemRegex);
if (match) {
linesToHandle.push(line);
}
}
}
},
});
linesToHandle.sort((a, b) => a.number - b.number);
handleLines(linesToHandle);
// Re-position the selection in a way that makes sense
if (sel.empty) {
sel = EditorSelection.cursor(toLine.to + charsAdded);
} else {
sel = EditorSelection.range(
fromLine.from,
toLine.to + charsAdded
);
}
return {
range: sel,
changes,
};
};

View File

@@ -1,236 +0,0 @@
//
// Exports a list of languages that can be used in fenced code blocks.
//
import { LanguageDescription, LanguageSupport, StreamParser } from '@codemirror/language';
import { StreamLanguage } from '@codemirror/language';
import { python } from '@codemirror/legacy-modes/mode/python';
import { c, dart } from '@codemirror/legacy-modes/mode/clike';
import { lua } from '@codemirror/legacy-modes/mode/lua';
import { r } from '@codemirror/legacy-modes/mode/r';
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
import { swift } from '@codemirror/legacy-modes/mode/swift';
import { go } from '@codemirror/legacy-modes/mode/go';
import { vb } from '@codemirror/legacy-modes/mode/vb';
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
import { css } from '@codemirror/legacy-modes/mode/css';
import { stex } from '@codemirror/legacy-modes/mode/stex';
import { groovy } from '@codemirror/legacy-modes/mode/groovy';
import { perl } from '@codemirror/legacy-modes/mode/perl';
import { cobol } from '@codemirror/legacy-modes/mode/cobol';
import { julia } from '@codemirror/legacy-modes/mode/julia';
import { haskell } from '@codemirror/legacy-modes/mode/haskell';
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
import { xml } from '@codemirror/legacy-modes/mode/xml';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { diff } from '@codemirror/legacy-modes/mode/diff';
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
import { sqlite, standardSQL, mySQL } from '@codemirror/legacy-modes/mode/sql';
import { javascript } from '@codemirror/lang-javascript';
import { markdown } from '@codemirror/lang-markdown';
import { html } from '@codemirror/lang-html';
import { cpp } from '@codemirror/lang-cpp';
import { php } from '@codemirror/lang-php';
import { java } from '@codemirror/lang-java';
import { rust } from '@codemirror/lang-rust';
const supportedLanguages: {
name: string;
aliases?: string[];
// Either support or parser must be given
parser?: StreamParser<any>;
support?: LanguageSupport;
}[] = [
// Based on @joplin/desktop/CodeMirror/Editor.tsx
{
name: 'LaTeX',
aliases: ['tex', 'latex', 'luatex'],
parser: stex,
},
{
name: 'python',
aliases: ['py'],
parser: python,
},
{
name: 'clike',
aliases: ['c', 'h'],
parser: c,
},
{
name: 'C++',
aliases: ['cpp', 'hpp', 'cxx', 'hxx', 'c++'],
support: cpp(),
},
{
name: 'java',
support: java(),
},
{
name: 'javascript',
aliases: ['js', 'mjs'],
support: javascript(),
},
{
name: 'typescript',
aliases: ['ts'],
support: javascript({ jsx: false, typescript: true }),
},
{
name: 'react javascript',
aliases: ['jsx'],
support: javascript({ jsx: true, typescript: false }),
},
{
name: 'react typescript',
aliases: ['tsx'],
support: javascript({ jsx: true, typescript: true }),
},
{
name: 'lua',
parser: lua,
},
{
name: 'php',
support: php(),
},
{
name: 'r',
parser: r,
},
{
name: 'swift',
parser: swift,
},
{
name: 'go',
parser: go,
},
{
name: 'visualbasic',
aliases: ['vb'],
parser: vb,
},
{
name: 'visualbasicscript',
aliases: ['vbscript', 'vbs'],
parser: vbScript,
},
{
name: 'ruby',
aliases: ['rb'],
parser: ruby,
},
{
name: 'rust',
aliases: ['rs'],
support: rust(),
},
{
name: 'dart',
parser: dart,
},
{
name: 'groovy',
parser: groovy,
},
{
name: 'perl',
aliases: ['pl'],
parser: perl,
},
{
name: 'cobol',
aliases: ['cbl', 'cob'],
parser: cobol,
},
{
name: 'julia',
aliases: ['jl'],
parser: julia,
},
{
name: 'haskell',
aliases: ['hs'],
parser: haskell,
},
{
name: 'pascal',
parser: pascal,
},
{
name: 'css',
parser: css,
},
{
name: 'xml',
aliases: ['xhtml'],
parser: xml,
},
{
name: 'html',
aliases: ['html', 'htm'],
support: html(),
},
{
name: 'markdown',
support: markdown(),
},
{
name: 'yaml',
parser: yaml,
},
{
name: 'shell',
aliases: ['bash', 'sh', 'zsh', 'dash'],
parser: shell,
},
{
name: 'dockerfile',
parser: dockerFile,
},
{
name: 'diff',
parser: diff,
},
{
name: 'erlang',
parser: erlang,
},
{
name: 'sql',
parser: standardSQL,
},
{
name: 'sqlite',
parser: sqlite,
},
{
name: 'mysql',
parser: mySQL,
},
];
// Convert supportedLanguages to a CodeMirror-readable list
// of LanguageDescriptions
const syntaxHighlightingLanguages: LanguageDescription[] = [];
for (const language of supportedLanguages) {
// Convert from parsers to LanguageSupport objects as necessary
const support = language.support ?? new LanguageSupport(StreamLanguage.define(language.parser));
syntaxHighlightingLanguages.push(
LanguageDescription.of({
name: language.name,
alias: language.aliases,
support,
})
);
}
export default syntaxHighlightingLanguages;

View File

@@ -1,234 +0,0 @@
//
// Create a set of Extensions that provide syntax highlighting.
//
import { defaultHighlightStyle, syntaxHighlighting, HighlightStyle } from '@codemirror/language';
import { tags } from '@lezer/highlight';
import { EditorView } from '@codemirror/view';
import { Extension } from '@codemirror/state';
import { inlineMathTag, mathTag } from './markdownMathParser';
// For an example on how to customize the theme, see:
//
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
//
// For a tutorial, see:
//
// https://codemirror.net/6/examples/styling/#themes
//
// Use Safari developer tools to view the content of the CodeMirror iframe while
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
// use '&.cm-focused' in the theme.
//
// [theme] should be a joplin theme (see @joplin/lib/theme)
const createTheme = (theme: any): Extension[] => {
const isDarkTheme = theme.appearance === 'dark';
const baseGlobalStyle: Record<string, string> = {
color: theme.color,
backgroundColor: theme.backgroundColor,
// On iOS, apply system font scaling (e.g. font scaling
// set in accessibility settings).
font: '-apple-system-body',
};
const baseCursorStyle: Record<string, string> = { };
const baseContentStyle: Record<string, string> = {
fontFamily: theme.fontFamily,
// To allow accessibility font scaling, we also need to set the
// fontSize to a value in `em`s (relative scaling relative to
// parent font size).
fontSize: `${theme.fontSize}em`,
};
const baseSelectionStyle: Record<string, string> = { };
const blurredSelectionStyle: Record<string, string> = { };
// If we're in dark mode, the caret and selection are difficult to see.
// Adjust them appropriately
if (isDarkTheme) {
// Styling the caret requires styling both the caret itself
// and the CodeMirror caret.
// See https://codemirror.net/6/examples/styling/#themes
baseContentStyle.caretColor = 'white';
baseCursorStyle.borderLeftColor = 'white';
baseSelectionStyle.backgroundColor = '#6b6b6b';
blurredSelectionStyle.backgroundColor = '#444';
}
const baseTheme = EditorView.baseTheme({
'&': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
'.cm-content': {
fontFamily: theme.fontFamily,
...baseContentStyle,
},
'&.cm-focused .cm-cursor': baseCursorStyle,
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
'.cm-selectionBackground': blurredSelectionStyle,
'&.cm-focused': {
outline: 'none',
},
'& .cm-blockQuote': {
borderLeft: `4px solid ${theme.colorFaded}`,
opacity: theme.blockQuoteOpacity,
paddingLeft: '4px',
},
'& .cm-codeBlock': {
'&.cm-regionFirstLine, &.cm-regionLastLine': {
borderRadius: '3px',
},
'&:not(.cm-regionFirstLine)': {
borderTop: 'none',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
'&:not(.cm-regionLastLine)': {
borderBottom: 'none',
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
borderWidth: '1px',
borderStyle: 'solid',
borderColor: theme.colorFaded,
backgroundColor: 'rgba(155, 155, 155, 0.1)',
},
// CodeMirror wraps the existing inline span in an additional element.
// Due to a Chrome rendering bug, because the .cm-inlineCode wraps a
// span with a larger font-size, the .cm-inlineCode's bounding box won't
// be big enough for its content.
// As such, we need to style whichever element directly wraps its content.
'& .cm-headerLine > .cm-inlineCode > *, & :not(.cm-headerLine) > .cm-inlineCode': {
borderWidth: '1px',
borderStyle: 'solid',
borderColor: isDarkTheme ? 'rgba(200, 200, 200, 0.5)' : 'rgba(100, 100, 100, 0.5)',
borderRadius: '4px',
},
'& .cm-mathBlock, & .cm-inlineMath': {
color: isDarkTheme ? '#9fa' : '#276',
},
// Style the search widget. Use ':root' to increase the selector's precedence
// (override the existing preset styles).
':root & .cm-panel.cm-search': {
'& label, & button, & input': {
fontSize: '1em',
color: isDarkTheme ? 'white' : 'black',
},
},
});
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
const baseHeadingStyle = {
fontWeight: 'bold',
fontFamily: theme.fontFamily,
};
const highlightingStyle = HighlightStyle.define([
{
tag: tags.strong,
fontWeight: 'bold',
},
{
tag: tags.emphasis,
fontStyle: 'italic',
},
{
...baseHeadingStyle,
tag: tags.heading1,
fontSize: '1.6em',
borderBottom: `1px solid ${theme.dividerColor}`,
},
{
...baseHeadingStyle,
tag: tags.heading2,
fontSize: '1.4em',
},
{
...baseHeadingStyle,
tag: tags.heading3,
fontSize: '1.3em',
},
{
...baseHeadingStyle,
tag: tags.heading4,
fontSize: '1.2em',
},
{
...baseHeadingStyle,
tag: tags.heading5,
fontSize: '1.1em',
},
{
...baseHeadingStyle,
tag: tags.heading6,
fontSize: '1.0em',
},
{
tag: tags.list,
fontFamily: theme.fontFamily,
},
{
tag: tags.comment,
opacity: 0.9,
fontStyle: 'italic',
},
{
tag: tags.link,
color: theme.urlColor,
textDecoration: 'underline',
},
{
tag: [mathTag, inlineMathTag],
fontStyle: 'italic',
},
// Content of code blocks
{
tag: tags.keyword,
color: isDarkTheme ? '#ff7' : '#740',
},
{
tag: tags.operator,
color: isDarkTheme ? '#f7f' : '#805',
},
{
tag: tags.literal,
color: isDarkTheme ? '#aaf' : '#037',
},
{
tag: tags.operator,
color: isDarkTheme ? '#fa9' : '#490',
},
{
tag: tags.typeName,
color: isDarkTheme ? '#7ff' : '#a00',
},
]);
return [
baseTheme,
appearanceTheme,
syntaxHighlighting(highlightingStyle),
// If we haven't defined highlighting for tags, fall back
// to the default.
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
];
};
export default createTheme;

View File

@@ -1,29 +0,0 @@
import { ListType, SearchControl } from '../types';
// Controls for the CodeMirror portion of the editor
export interface CodeMirrorControl {
undo(): void;
redo(): void;
select(anchor: number, head: number): void;
insertText(text: string): void;
setSpellcheckEnabled(enabled: boolean): void;
// Toggle whether we're in a type of region.
toggleBolded(): void;
toggleItalicized(): void;
toggleList(kind: ListType): void;
toggleCode(): void;
toggleMath(): void;
toggleHeaderLevel(level: number): void;
// Create a new link or update the currently selected link with
// the given [label] and [url].
updateLink(label: string, url: string): void;
increaseIndent(): void;
decreaseIndent(): void;
scrollSelectionIntoView(): void;
searchControl: SearchControl;
}

View File

@@ -1,19 +0,0 @@
// Handle logging strings when running in a WebView.
// Because this will be running both in a WebView and in nodeJS, we need to use
// globalThis in place of window. We need to tell ESLint that we're doing this:
/* global globalThis*/
export function postMessage(name: string, data: any) {
// Only call postMessage if we're running in a WebView (this code may be called
// in integration tests).
(globalThis as any).ReactNativeWebView?.postMessage(JSON.stringify({
data,
name,
}));
}
export function logMessage(...msg: any[]) {
postMessage('onLog', { value: msg });
}

View File

@@ -1,156 +0,0 @@
// Dialog allowing the user to update/create links
const React = require('react');
const { useState, useEffect, useMemo, useRef } = require('react');
const { StyleSheet } = require('react-native');
const { View, Modal, Text, TextInput, Button } = require('react-native');
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import { EditorControl } from './types';
import SelectionFormatting from './SelectionFormatting';
import { useCallback } from 'react';
interface LinkDialogProps {
editorControl: EditorControl;
selectionState: SelectionFormatting;
visible: boolean;
themeId: number;
}
const EditLinkDialog = (props: LinkDialogProps) => {
// The content of the link selected in the editor (if any)
const editorLinkData = props.selectionState.linkData;
const [linkLabel, setLinkLabel] = useState('');
const [linkURL, setLinkURL] = useState('');
const linkInputRef = useRef();
// Reset the label and URL when shown/hidden
useEffect(() => {
setLinkLabel(editorLinkData.linkText ?? props.selectionState.selectedText);
setLinkURL(editorLinkData.linkURL ?? '');
}, [
props.visible, editorLinkData.linkText, props.selectionState.selectedText,
editorLinkData.linkURL,
]);
const [styles, placeholderColor] = useMemo(() => {
const theme = themeStyle(props.themeId);
const styleSheet = StyleSheet.create({
modalContent: {
margin: 15,
padding: 30,
backgroundColor: theme.backgroundColor,
elevation: 5,
shadowOffset: {
width: 1,
height: 1,
},
shadowOpacity: 0.4,
shadowRadius: 1,
},
button: {
color: theme.color2,
backgroundColor: theme.backgroundColor2,
},
text: {
color: theme.color,
},
header: {
color: theme.color,
fontSize: 22,
},
input: {
color: theme.color,
backgroundColor: theme.backgroundColor,
minHeight: 48,
borderBottomColor: theme.backgroundColor3,
borderBottomWidth: 1,
},
inputContainer: {
flexDirection: 'column',
paddingBottom: 10,
},
});
const placeholderColor = theme.colorFaded;
return [styleSheet, placeholderColor];
}, [props.themeId]);
const onSubmit = useCallback(() => {
props.editorControl.updateLink(linkLabel, linkURL);
props.editorControl.hideLinkDialog();
}, [props.editorControl, linkLabel, linkURL]);
// See https://www.hingehealth.com/engineering-blog/accessible-react-native-textinput/
// for more about creating accessible RN inputs.
const linkTextInput = (
<View style={styles.inputContainer} accessible>
<Text style={styles.text}>{_('Link Text')}</Text>
<TextInput
style={styles.input}
placeholder={_('Description of the link')}
placeholderTextColor={placeholderColor}
value={linkLabel}
returnKeyType="next"
autoFocus
onSubmitEditing={() => {
linkInputRef.current.focus();
}}
onChangeText={(text: string) => setLinkLabel(text)}
/>
</View>
);
const linkURLInput = (
<View style={styles.inputContainer} accessible>
<Text style={styles.text}>{_('URL')}</Text>
<TextInput
style={styles.input}
placeholder={_('URL')}
placeholderTextColor={placeholderColor}
value={linkURL}
ref={linkInputRef}
autoCorrect={false}
autoCapitalize="none"
keyboardType="url"
textContentType="URL"
returnKeyType="done"
onSubmitEditing={onSubmit}
onChangeText={(text: string) => setLinkURL(text)}
/>
</View>
);
return (
<Modal
animationType="slide"
transparent={true}
visible={props.visible}
onRequestClose={() => {
props.editorControl.hideLinkDialog();
}}>
<View style={styles.modalContent}>
<Text style={styles.header}>{_('Edit Link')}</Text>
<View>
{linkTextInput}
{linkURLInput}
</View>
<Button
style={styles.button}
onPress={onSubmit}
title={_('Done')}
/>
</View>
</Modal>
);
};
export default EditLinkDialog;

View File

@@ -1,26 +1,28 @@
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import { themeStyle } from '@joplin/lib/theme';
import EditLinkDialog from './EditLinkDialog';
import { defaultSearchState, SearchPanel } from './SearchPanel';
const React = require('react');
const { forwardRef, useImperativeHandle } = require('react');
const { useEffect, useMemo, useState, useCallback, useRef } = require('react');
const { forwardRef, useImperativeHandle, useEffect, useMemo, useState, useCallback, useRef } = require('react');
const { WebView } = require('react-native-webview');
const { View } = require('react-native');
const { editorFont } = require('../global-style');
import SelectionFormatting from './SelectionFormatting';
import {
EditorSettings,
EditorControl,
export interface ChangeEvent {
value: string;
}
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent,
ListType,
SearchState,
} from './types';
import { _ } from '@joplin/lib/locale';
export interface UndoRedoDepthChangeEvent {
undoDepth: number;
redoDepth: number;
}
export interface Selection {
start: number;
end: number;
}
export interface SelectionChangeEvent {
selection: Selection;
}
type ChangeEventHandler = (event: ChangeEvent)=> void;
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
@@ -38,10 +40,149 @@ interface Props {
}
function fontFamilyFromSettings() {
const font = editorFont(Setting.value('style.editor.fontFamily'));
return font ? `${font}, sans-serif` : 'sans-serif';
const f = editorFont(Setting.value('style.editor.fontFamily'));
return [f, 'sans-serif'].join(', ');
}
// Obsolete with CodeMirror 6. See ./CodeMirror.ts for styling.
// function useCss(themeId:number):string {
// const [css, setCss] = useState('');
// // useEffect(() => {
// // const theme = themeStyle(themeId);
// // // Selection in dark mode is hard to see so make it brighter.
// // // https://discourse.joplinapp.org/t/dragging-in-dark-theme/12433/4?u=laurent
// // const selectionColorCss = theme.appearance === ThemeAppearance.Dark ?
// // `.CodeMirror-selected {
// // background: #6b6b6b !important;
// // }` : '';
// // const monospaceFonts = [];
// // // if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
// // monospaceFonts.push('monospace');
// // const fontSize = 15;
// // const fontFamily = fontFamilyFromSettings();
// // // BUG: caret-color seems to be ignored for some reason
// // const caretColor = theme.appearance === ThemeAppearance.Dark ? "white" : 'black';
// // setCss(`
// // /* These must be important to prevent the codemirror defaults from taking over*/
// // .CodeMirror {
// // font-family: ${fontFamily};
// // font-size: ${fontSize}px;
// // height: 100% !important;
// // width: 100% !important;
// // color: ${theme.color};
// // background-color: ${theme.backgroundColor};
// // position: absolute !important;
// // -webkit-box-shadow: none !important; // Some themes add a box shadow for some reason
// // }
// // .CodeMirror-lines {
// // /* This is used to enable the scroll-past end behaviour. The same height should */
// // /* be applied to the viewer. */
// // padding-bottom: 400px !important;
// // }
// // /* Left padding is applied at the editor component level, so we should remove it from the lines */
// // .CodeMirror pre.CodeMirror-line,
// // .CodeMirror pre.CodeMirror-line-like {
// // padding-left: 0;
// // }
// // .CodeMirror-sizer {
// // /* Add a fixed right padding to account for the appearance (and disappearance) */
// // /* of the sidebar */
// // padding-right: 10px !important;
// // }
// // /* This enforces monospace for certain elements (code, tables, etc.) */
// // .cm-jn-monospace {
// // font-family: ${monospaceFonts.join(', ')} !important;
// // }
// // .cm-header-1 {
// // font-size: 1.5em;
// // }
// // .cm-header-2 {
// // font-size: 1.3em;
// // }
// // .cm-header-3 {
// // font-size: 1.1em;
// // }
// // .cm-header-4, .cm-header-5, .cm-header-6 {
// // font-size: 1em;
// // }
// // .cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 {
// // line-height: 1.5em;
// // }
// // .cm-search-marker {
// // background: ${theme.searchMarkerBackgroundColor};
// // color: ${theme.searchMarkerColor} !important;
// // }
// // .cm-search-marker-selected {
// // background: ${theme.selectedColor2};
// // color: ${theme.color2} !important;
// // }
// // .cm-search-marker-scrollbar {
// // background: ${theme.searchMarkerBackgroundColor};
// // -moz-box-sizing: border-box;
// // box-sizing: border-box;
// // opacity: .5;
// // }
// // /* We need to use important to override theme specific values */
// // .cm-error {
// // color: inherit !important;
// // background-color: inherit !important;
// // border-bottom: 1px dotted #dc322f;
// // }
// // /* The default dark theme colors don't have enough contrast with the background */
// // .cm-s-nord span.cm-comment {
// // color: #9aa4b6 !important;
// // }
// // .cm-s-dracula span.cm-comment {
// // color: #a1abc9 !important;
// // }
// // .cm-s-monokai span.cm-comment {
// // color: #908b74 !important;
// // }
// // .cm-s-material-darker span.cm-comment {
// // color: #878787 !important;
// // }
// // .cm-s-solarized.cm-s-dark span.cm-comment {
// // color: #8ba1a7 !important;
// // }
// // /* MOBILE SPECIFIC */
// // .CodeMirror .cm-scroller,
// // .CodeMirror .cm-line {
// // font-family: ${fontFamily};
// // caret-color: ${caretColor};
// // }
// // ${selectionColorCss}
// // `);
// // }, [themeId]);
// return css;
// }
function useCss(themeId: number): string {
return useMemo(() => {
const theme = themeStyle(themeId);
@@ -49,22 +190,6 @@ function useCss(themeId: number): string {
:root {
background-color: ${theme.backgroundColor};
}
body {
margin: 0;
height: 100vh;
width: 100vh;
width: 100vw;
min-width: 100vw;
box-sizing: border-box;
padding-left: 1px;
padding-right: 1px;
padding-bottom: 1px;
padding-top: 10px;
font-size: 13pt;
}
`;
}, [themeId]);
}
@@ -72,27 +197,28 @@ function useCss(themeId: number): string {
function useHtml(css: string): string {
const [html, setHtml] = useState('');
useMemo(() => {
setHtml(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>${_('Note editor')}</title>
<style>
.cm-editor {
height: 100%;
}
useEffect(() => {
setHtml(
`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<style>
.cm-editor {
height: 100%;
}
${css}
</style>
</head>
<body>
<div class="CodeMirror" style="height:100%;" autocapitalize="on"></div>
</body>
</html>
`);
${css}
</style>
</head>
<body style="margin:0; height:100vh; width:100vh; width:100vw; min-width:100vw; box-sizing: border-box; padding: 10px;">
<div class="CodeMirror" style="height:100%;" autocapitalize="on"></div>
</body>
</html>
`
);
}, [css]);
return html;
@@ -101,7 +227,7 @@ function useHtml(css: string): string {
function editorTheme(themeId: number) {
return {
...themeStyle(themeId),
fontSize: 0.85, // em
fontSize: 15,
fontFamily: fontFamilyFromSettings(),
};
}
@@ -114,11 +240,6 @@ function NoteEditor(props: Props, ref: any) {
cm.select(${props.initialSelection.start}, ${props.initialSelection.end});
` : '';
const editorSettings: EditorSettings = {
themeData: editorTheme(props.themeId),
katexEnabled: Setting.value('markdown.plugin.katex') as boolean,
};
const injectedJavaScript = `
function postMessage(name, data) {
window.ReactNativeWebView.postMessage(JSON.stringify({
@@ -131,158 +252,47 @@ function NoteEditor(props: Props, ref: any) {
postMessage('onLog', { value: msg });
}
// Globalize logMessage, postMessage
window.logMessage = logMessage;
window.postMessage = postMessage;
// This variable is not used within this script
// but is called using "injectJavaScript" from
// the wrapper component.
window.cm = null;
window.onerror = (message, source, lineno) => {
window.ReactNativeWebView.postMessage(
"error: " + message + " in file://" + source + ", line " + lineno
);
};
try {
${shim.injectedJs('codeMirrorBundle')};
if (!window.cm) {
// This variable is not used within this script
// but is called using "injectJavaScript" from
// the wrapper component.
window.cm = null;
const parentElement = document.getElementsByClassName('CodeMirror')[0];
const theme = ${JSON.stringify(editorTheme(props.themeId))};
const initialText = ${JSON.stringify(props.initialText)};
try {
${shim.injectedJs('codeMirrorBundle')};
const parentElement = document.getElementsByClassName('CodeMirror')[0];
const initialText = ${JSON.stringify(props.initialText)};
const settings = ${JSON.stringify(editorSettings)};
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, settings);
${setInitialSelectionJS}
window.onresize = () => {
cm.scrollSelectionIntoView();
};
} catch (e) {
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
}
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, theme);
${setInitialSelectionJS}
} catch (e) {
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
} finally {
true;
}
true;
`;
const css = useCss(props.themeId);
const html = useHtml(css);
const [selectionState, setSelectionState] = useState(new SelectionFormatting());
const [searchState, setSearchState] = useState(defaultSearchState);
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
// / Runs [js] in the context of the CodeMirror frame.
const injectJS = (js: string) => {
webviewRef.current.injectJavaScript(`
try {
${js}
}
catch(e) {
logMessage('Error in injected JS:' + e, e);
throw e;
};
true;`);
};
const editorControl: EditorControl = {
undo() {
injectJS('cm.undo();');
},
redo() {
injectJS('cm.redo();');
},
select(anchor: number, head: number) {
injectJS(
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)});`
);
},
insertText(text: string) {
injectJS(`cm.insertText(${JSON.stringify(text)});`);
},
toggleBolded() {
injectJS('cm.toggleBolded();');
},
toggleItalicized() {
injectJS('cm.toggleItalicized();');
},
toggleList(listType: ListType) {
injectJS(`cm.toggleList(${JSON.stringify(listType)});`);
},
toggleCode() {
injectJS('cm.toggleCode();');
},
toggleMath() {
injectJS('cm.toggleMath();');
},
toggleHeaderLevel(level: number) {
injectJS(`cm.toggleHeaderLevel(${level});`);
},
increaseIndent() {
injectJS('cm.increaseIndent();');
},
decreaseIndent() {
injectJS('cm.decreaseIndent();');
},
updateLink(label: string, url: string) {
injectJS(`cm.updateLink(
${JSON.stringify(label)},
${JSON.stringify(url)}
);`);
},
scrollSelectionIntoView() {
injectJS('cm.scrollSelectionIntoView();');
},
showLinkDialog() {
setLinkDialogVisible(true);
},
hideLinkDialog() {
setLinkDialogVisible(false);
},
hideKeyboard() {
injectJS('document.activeElement?.blur();');
},
setSpellcheckEnabled(enabled: boolean) {
injectJS(`cm.setSpellcheckEnabled(${enabled ? 'true' : 'false'});`);
},
searchControl: {
findNext() {
injectJS('cm.searchControl.findNext();');
},
findPrevious() {
injectJS('cm.searchControl.findPrevious();');
},
replaceCurrent() {
injectJS('cm.searchControl.replaceCurrent();');
},
replaceAll() {
injectJS('cm.searchControl.replaceAll();');
},
setSearchState(state: SearchState) {
injectJS(`cm.searchControl.setSearchState(${JSON.stringify(state)})`);
setSearchState(state);
},
showSearch() {
const newSearchState: SearchState = Object.assign({}, searchState);
newSearchState.dialogVisible = true;
setSearchState(newSearchState);
},
hideSearch() {
const newSearchState: SearchState = Object.assign({}, searchState);
newSearchState.dialogVisible = false;
setSearchState(newSearchState);
},
},
};
useImperativeHandle(ref, () => {
return editorControl;
return {
undo: function() {
webviewRef.current.injectJavaScript('cm.undo(); true;');
},
redo: function() {
webviewRef.current.injectJavaScript('cm.redo(); true;');
},
select: (anchor: number, head: number) => {
webviewRef.current.injectJavaScript(
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)}); true;`
);
},
insertText: (text: string) => {
webviewRef.current.injectJavaScript(`cm.insertText(${JSON.stringify(text)}); true;`);
},
};
});
useEffect(() => {
@@ -332,26 +342,6 @@ function NoteEditor(props: Props, ref: any) {
onSelectionChange: (event: SelectionChangeEvent) => {
props.onSelectionChange(event);
},
onSelectionFormattingChange(data: string) {
// We want a SelectionFormatting object, so are
// instantiating it from JSON.
const formatting = SelectionFormatting.fromJSON(data);
setSelectionState(formatting);
},
onRequestLinkEdit() {
editorControl.showLinkDialog();
},
onRequestShowSearch(data: SearchState) {
setSearchState(data);
editorControl.searchControl.showSearch();
},
onRequestHideSearch() {
editorControl.searchControl.hideSearch();
},
};
if (handlers[msg.name]) {
@@ -365,53 +355,21 @@ function NoteEditor(props: Props, ref: any) {
console.error('NoteEditor: webview error');
});
// - `setSupportMultipleWindows` must be `true` for security reasons:
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
// when the editor is focused.
return (
<View style={{
...props.style,
flexDirection: 'column',
}}>
<EditLinkDialog
visible={linkDialogVisible}
themeId={props.themeId}
editorControl={editorControl}
selectionState={selectionState}
/>
<View style={{
flexGrow: 1,
flexShrink: 0,
minHeight: '40%',
}}>
<WebView
style={{
backgroundColor: editorSettings.themeData.backgroundColor,
}}
ref={webviewRef}
scrollEnabled={false}
useWebKit={true}
source={source}
setSupportMultipleWindows={true}
hideKeyboardAccessoryView={true}
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
allowFileAccess={true}
injectedJavaScript={injectedJavaScript}
onMessage={onMessage}
onError={onError}
/>
</View>
<SearchPanel
editorSettings={editorSettings}
searchControl={editorControl.searchControl}
searchState={searchState}
/>
</View>
);
return <WebView
style={props.style}
ref={webviewRef}
useWebKit={true}
source={source}
setSupportMultipleWindows={true}
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
allowFileAccess={true}
injectedJavaScript={injectedJavaScript}
onMessage={onMessage}
onError={onError}
/>;
}
export default forwardRef(NoteEditor);

View File

@@ -1,355 +0,0 @@
// Displays a find/replace dialog
const React = require('react');
const { StyleSheet } = require('react-native');
const { TextInput, View, Text, TouchableOpacity } = require('react-native');
const { useMemo, useState, useEffect } = require('react');
const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default;
import { SearchControl, SearchState, EditorSettings } from './types';
import { _ } from '@joplin/lib/locale';
import { BackHandler } from 'react-native';
import { Theme } from '@joplin/lib/themes/type';
const buttonSize = 48;
type OnChangeCallback = (text: string)=> void;
type Callback = ()=> void;
export const defaultSearchState: SearchState = {
useRegex: false,
caseSensitive: false,
searchText: '',
replaceText: '',
dialogVisible: false,
};
export interface SearchPanelProps {
searchControl: SearchControl;
searchState: SearchState;
editorSettings: EditorSettings;
}
interface ActionButtonProps {
styles: any;
iconName: string;
title: string;
onPress: Callback;
}
const ActionButton = (
props: ActionButtonProps
) => {
return (
<TouchableOpacity
style={props.styles.button}
onPress={props.onPress}
accessibilityLabel={props.title}
accessibilityRole='button'
>
<MaterialCommunityIcon name={props.iconName} style={props.styles.buttonText}/>
</TouchableOpacity>
);
};
interface ToggleButtonProps {
styles: any;
iconName: string;
title: string;
active: boolean;
onToggle: Callback;
}
const ToggleButton = (props: ToggleButtonProps) => {
const active = props.active;
return (
<TouchableOpacity
style={{
...props.styles.toggleButton,
...(active ? props.styles.toggleButtonActive : {}),
}}
onPress={props.onToggle}
accessibilityState={{
checked: props.active,
}}
accessibilityLabel={props.title}
accessibilityRole='switch'
>
<MaterialCommunityIcon name={props.iconName} style={
active ? props.styles.activeButtonText : props.styles.buttonText
}/>
</TouchableOpacity>
);
};
const useStyles = (theme: Theme) => {
return useMemo(() => {
const buttonStyle = {
width: buttonSize,
height: buttonSize,
backgroundColor: theme.backgroundColor4,
alignItems: 'center',
justifyContent: 'center',
flexShrink: 1,
};
const buttonTextStyle = {
color: theme.color4,
fontSize: 30,
};
return StyleSheet.create({
button: buttonStyle,
toggleButton: {
...buttonStyle,
},
toggleButtonActive: {
...buttonStyle,
backgroundColor: theme.backgroundColor3,
},
input: {
flexGrow: 1,
height: buttonSize,
backgroundColor: theme.backgroundColor4,
color: theme.color4,
},
buttonText: buttonTextStyle,
activeButtonText: {
...buttonTextStyle,
color: theme.color4,
},
text: {
color: theme.color,
},
labeledInput: {
flexGrow: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginLeft: 10,
},
});
}, [theme]);
};
export const SearchPanel = (props: SearchPanelProps) => {
const placeholderColor = props.editorSettings.themeData.color3;
const styles = useStyles(props.editorSettings.themeData);
const [showingAdvanced, setShowAdvanced] = useState(false);
const state = props.searchState;
const control = props.searchControl;
const updateSearchState = (changedData: any) => {
const newState = Object.assign({}, state, changedData);
control.setSearchState(newState);
};
// Creates a TextInut with the given parameters
const createInput = (
placeholder: string, value: string, onChange: OnChangeCallback, autoFocus: boolean
) => {
return (
<TextInput
style={styles.input}
autoFocus={autoFocus}
onChangeText={onChange}
value={value}
placeholder={placeholder}
placeholderTextColor={placeholderColor}
returnKeyType='search'
blurOnSubmit={false}
onSubmitEditing={control.findNext}
/>
);
};
// Close the search dialog on back button press
useEffect(() => {
// Only register the listener if the dialog is visible
if (!state.dialogVisible) {
return () => {};
}
const backListener = BackHandler.addEventListener('hardwareBackPress', () => {
control.hideSearch();
return true;
});
return () => backListener.remove();
}, [state.dialogVisible]);
const closeButton = (
<ActionButton
styles={styles}
iconName="close"
onPress={control.hideSearch}
title={_('Close search')}
/>
);
const showDetailsButton = (
<ActionButton
styles={styles}
iconName="menu-down"
onPress={() => setShowAdvanced(true)}
title={_('Show advanced')}
/>
);
const hideDetailsButton = (
<ActionButton
styles={styles}
iconName="menu-up"
onPress={() => setShowAdvanced(false)}
title={_('Hide advanced')}
/>
);
const searchTextInput = createInput(
_('Search for...'),
state.searchText,
(newText: string) => {
updateSearchState({
searchText: newText,
});
},
// Autofocus
true
);
const replaceTextInput = createInput(
_('Replace with...'),
state.replaceText,
(newText: string) => {
updateSearchState({
replaceText: newText,
});
},
// Don't autofocus
false
);
const labeledSearchInput = (
<View style={styles.labeledInput} accessible>
<Text style={styles.text}>{_('Find: ')}</Text>
{ searchTextInput }
</View>
);
const labeledReplaceInput = (
<View style={styles.labeledInput} accessible>
<Text style={styles.text}>{_('Replace: ')}</Text>
{ replaceTextInput }
</View>
);
const toNextButton = (
<ActionButton
styles={styles}
iconName="menu-right"
onPress={control.findNext}
title={_('Next match')}
/>
);
const toPrevButton = (
<ActionButton
styles={styles}
iconName="menu-left"
onPress={control.findPrevious}
title={_('Previous match')}
/>
);
const replaceButton = (
<ActionButton
styles={styles}
iconName="swap-horizontal"
onPress={control.replaceCurrent}
title={_('Replace')}
/>
);
const replaceAllButton = (
<ActionButton
styles={styles}
iconName="reply-all"
onPress={control.replaceAll}
title={_('Replace all')}
/>
);
const regexpButton = (
<ToggleButton
styles={styles}
iconName="regex"
onToggle={() => {
updateSearchState({
useRegex: !state.useRegex,
});
}}
active={state.useRegex}
title={_('Regular expression')}
/>
);
const caseSensitiveButton = (
<ToggleButton
styles={styles}
iconName="format-letter-case"
onToggle={() => {
updateSearchState({
caseSensitive: !state.caseSensitive,
});
}}
active={state.caseSensitive}
title={_('Case sensitive')}
/>
);
const simpleLayout = (
<View style={{ flexDirection: 'row' }}>
{ closeButton }
{ searchTextInput }
{ showDetailsButton }
{ toPrevButton }
{ toNextButton }
</View>
);
const advancedLayout = (
<View style={{ flexDirection: 'column', alignItems: 'center' }}>
<View style={{ flexDirection: 'row' }}>
{ closeButton }
{ labeledSearchInput }
{ hideDetailsButton }
{ toPrevButton }
{ toNextButton }
</View>
<View style={{ flexDirection: 'row' }}>
{ regexpButton }
{ caseSensitiveButton }
{ labeledReplaceInput }
{ replaceButton }
{ replaceAllButton }
</View>
</View>
);
if (!state.dialogVisible) {
return null;
}
return showingAdvanced ? advancedLayout : simpleLayout;
};
export default SearchPanel;

View File

@@ -1,98 +0,0 @@
// Stores information about the current content of the user's selection
export default class SelectionFormatting {
public bolded: boolean = false;
public italicized: boolean = false;
public inChecklist: boolean = false;
public inCode: boolean = false;
public inUnorderedList: boolean = false;
public inOrderedList: boolean = false;
public inMath: boolean = false;
public inLink: boolean = false;
public spellChecking: boolean = false;
public unspellCheckableRegion: boolean = false;
// Link data, both fields are null if not in a link.
public linkData: { linkText?: string; linkURL?: string } = {
linkText: null,
linkURL: null,
};
// If [headerLevel], [listLevel], etc. are zero, then the
// selection isn't in a header/list
public headerLevel: number = 0;
public listLevel: number = 0;
// Content of the selection
public selectedText: string = '';
// List of data properties (for serializing/deseralizing)
private static propNames: string[] = [
'bolded', 'italicized', 'inChecklist', 'inCode',
'inUnorderedList', 'inOrderedList', 'inMath',
'inLink', 'linkData',
'headerLevel', 'listLevel',
'selectedText',
'spellChecking',
'unspellCheckableRegion',
];
// Returns true iff [this] is equivalent to [other]
public eq(other: SelectionFormatting): boolean {
// Cast to Records to allow usage of the indexing ([])
// operator.
const selfAsRec = this as Record<string, any>;
const otherAsRec = other as Record<string, any>;
for (const prop of SelectionFormatting.propNames) {
if (selfAsRec[prop] !== otherAsRec[prop]) {
return false;
}
}
return true;
}
public static fromJSON(json: string): SelectionFormatting {
const result = new SelectionFormatting();
// Casting result to a Record<string, any> lets us use
// the indexing [] operator.
const resultRecord = result as Record<string, any>;
const obj = JSON.parse(json) as Record<string, any>;
for (const prop of SelectionFormatting.propNames) {
if (obj[prop] !== undefined) {
// Type checking!
if (typeof obj[prop] !== typeof resultRecord[prop]) {
throw new Error([
'Deserialization Error:',
`${obj[prop]} and ${resultRecord[prop]}`,
'have different types.',
].join(' '));
}
resultRecord[prop] = obj[prop];
}
}
return result;
}
public toJSON(): string {
const resultObj: Record<string, any> = {};
// Cast this to a dictionary. This allows us to use
// the indexing [] operator.
const selfAsRecord = this as Record<string, any>;
for (const prop of SelectionFormatting.propNames) {
resultObj[prop] = selfAsRecord[prop];
}
return JSON.stringify(resultObj);
}
}

View File

@@ -1,61 +0,0 @@
// Types related to the NoteEditor
import { CodeMirrorControl } from './CodeMirror/types';
// Controls for the entire editor (including dialogs)
export interface EditorControl extends CodeMirrorControl {
showLinkDialog(): void;
hideLinkDialog(): void;
hideKeyboard(): void;
}
export interface EditorSettings {
themeData: any;
katexEnabled: boolean;
}
export interface ChangeEvent {
// New editor content
value: string;
}
export interface UndoRedoDepthChangeEvent {
undoDepth: number;
redoDepth: number;
}
export interface Selection {
start: number;
end: number;
}
export interface SelectionChangeEvent {
selection: Selection;
}
export interface SearchControl {
findNext(): void;
findPrevious(): void;
replaceCurrent(): void;
replaceAll(): void;
setSearchState(state: SearchState): void;
showSearch(): void;
hideSearch(): void;
}
export interface SearchState {
useRegex: boolean;
caseSensitive: boolean;
searchText: string;
replaceText: string;
dialogVisible: boolean;
}
// Possible types of lists in the editor
export enum ListType {
CheckList,
OrderedList,
UnorderedList,
}

View File

@@ -68,7 +68,7 @@ export default class SelectDateTimeDialog extends React.PureComponent<any, any>
}
UNSAFE_componentWillReceiveProps(newProps: any) {
if (newProps.date !== this.state.date) {
if (newProps.date != this.state.date) {
this.setState({ date: newProps.date });
}
}

View File

@@ -60,31 +60,12 @@ class ActionButtonComponent extends React.Component {
renderIconMultiStates() {
const button = this.props.buttons[this.state.buttonIndex];
return <Icon
name={button.icon}
style={styles.actionButtonIcon}
accessibilityLabel={button.title}
/>;
return <Icon name={button.icon} style={styles.actionButtonIcon} />;
}
renderIcon() {
const mainButton = this.props.mainButton ? this.props.mainButton : {};
const iconName = mainButton.icon ?? 'md-add';
// Icons don't have alt text by default. We need to add it:
const iconTitle = mainButton.title ?? _('Add new');
// TODO: If the button toggles a sub-menu, state whether the submenu is open
// or closed.
return (
<Icon
name={iconName}
style={styles.actionButtonIcon}
accessibilityLabel={iconTitle}
/>
);
return mainButton.icon ? <Icon name={mainButton.icon} style={styles.actionButtonIcon} /> : <Icon name="md-add" style={styles.actionButtonIcon} />;
}
render() {
@@ -118,14 +99,8 @@ class ActionButtonComponent extends React.Component {
const buttonTitle = button.title ? button.title : '';
const key = `${buttonTitle.replace(/\s/g, '_')}_${button.icon}`;
buttonComps.push(
// TODO: By default, ReactNativeActionButton also adds a title, which is focusable
// by the screen reader. As such, each item currently is double-focusable
<ReactNativeActionButton.Item key={key} buttonColor={button.color} title={buttonTitle} onPress={button.onPress}>
<Icon
name={button.icon}
style={styles.actionButtonIcon}
accessibilityLabel={buttonTitle}
/>
<Icon name={button.icon} style={styles.actionButtonIcon} />
</ReactNativeActionButton.Item>
);
}

View File

@@ -48,9 +48,9 @@ class AppNavComponent extends Component {
let notesScreenVisible = false;
let searchScreenVisible = false;
if (route.routeName === 'Notes') {
if (route.routeName == 'Notes') {
notesScreenVisible = true;
} else if (route.routeName === 'Search') {
} else if (route.routeName == 'Search') {
searchScreenVisible = true;
} else {
Screen = this.props.screens[route.routeName].screen;
@@ -59,7 +59,7 @@ class AppNavComponent extends Component {
// Keep the search screen loaded if the user is viewing a note from that search screen
// so that if the back button is pressed, the screen is still loaded. However, unload
// it if navigating away.
const searchScreenLoaded = searchScreenVisible || (this.previousRouteName_ === 'Search' && route.routeName === 'Note');
const searchScreenLoaded = searchScreenVisible || (this.previousRouteName_ == 'Search' && route.routeName == 'Note');
this.previousRouteName_ = route.routeName;

View File

@@ -61,14 +61,7 @@ class Checkbox extends Component {
// if (style.display) thStyle.display = style.display;
return (
<TouchableHighlight
onPress={() => this.onPress()}
style={thStyle}
accessibilityRole="checkbox"
accessibilityState={{
checked: this.state.checked,
}}
accessibilityLabel={this.props.accessibilityLabel ?? ''}>
<TouchableHighlight onPress={() => this.onPress()} style={thStyle}>
<Icon name={iconName} style={checkboxIconStyle} />
</TouchableHighlight>
);

View File

@@ -82,7 +82,7 @@ function editorFont(fontId) {
[Setting.FONT_MONOSPACE]: 'monospace',
};
if (!fontId) {
// console.warn('Editor font not set! Falling back to default font."');
console.warn('Editor font not set! Falling back to default font."');
fontId = Setting.FONT_DEFAULT;
}
return fonts[fontId];

View File

@@ -6,7 +6,6 @@ const { Checkbox } = require('./checkbox.js');
const Note = require('@joplin/lib/models/Note').default;
const time = require('@joplin/lib/time').default;
const { themeStyle } = require('./global-style.js');
const { _ } = require('@joplin/lib/locale');
class NoteItemComponent extends Component {
constructor() {
@@ -129,20 +128,13 @@ class NoteItemComponent extends Component {
const selectionWrapperStyle = isSelected ? this.styles().selectionWrapperSelected : this.styles().selectionWrapper;
const noteTitle = Note.displayTitle(note);
return (
<TouchableOpacity onPress={() => this.onPress()} onLongPress={() => this.onLongPress()} activeOpacity={0.5}>
<View style={selectionWrapperStyle}>
<View style={opacityStyle}>
<View style={listItemStyle}>
<Checkbox
style={checkboxStyle}
checked={checkboxChecked}
onChange={checked => this.todoCheckbox_change(checked)}
accessibilityLabel={_('to-do: %s', noteTitle)}
/>
<Text style={listItemTextStyle}>{noteTitle}</Text>
<Checkbox style={checkboxStyle} checked={checkboxChecked} onChange={checked => this.todoCheckbox_change(checked)} />
<Text style={listItemTextStyle}>{Note.displayTitle(note)}</Text>
</View>
</View>
</View>

View File

@@ -57,7 +57,7 @@ class NoteListComponent extends Component {
filterNotes(notes) {
const todoFilter = 'all'; // Setting.value('todoFilter');
if (todoFilter === 'all') return notes;
if (todoFilter == 'all') return notes;
const now = time.unixMs();
const maxInterval = 1000 * 60 * 60 * 24;
@@ -67,8 +67,8 @@ class NoteListComponent extends Component {
for (let i = 0; i < notes.length; i++) {
const note = notes[i];
if (note.is_todo) {
if (todoFilter === 'recent' && note.user_updated_time < notRecentTime && !!note.todo_completed) continue;
if (todoFilter === 'nonCompleted' && !!note.todo_completed) continue;
if (todoFilter == 'recent' && note.user_updated_time < notRecentTime && !!note.todo_completed) continue;
if (todoFilter == 'nonCompleted' && !!note.todo_completed) continue;
}
output.push(note);
}
@@ -77,7 +77,7 @@ class NoteListComponent extends Component {
UNSAFE_componentWillReceiveProps(newProps) {
// Make sure scroll position is reset when switching from one folder to another or to a tag list.
if (this.rootRef_ && newProps.notesSource !== this.props.notesSource) {
if (this.rootRef_ && newProps.notesSource != this.props.notesSource) {
this.rootRef_.scrollToOffset({ offset: 0, animated: false });
}
}

View File

@@ -29,8 +29,10 @@ class ScreenHeaderComponent extends React.PureComponent {
constructor() {
super();
this.styles_ = {};
this.state = { showUndoRedoButtons: true };
}
styles() {
const themeId = Setting.value('theme');
if (this.styles_[themeId]) return this.styles_[themeId];
@@ -197,7 +199,7 @@ class ScreenHeaderComponent extends React.PureComponent {
}
menu_select(value) {
if (typeof value === 'function') {
if (typeof value == 'function') {
value();
}
}
@@ -225,12 +227,7 @@ class ScreenHeaderComponent extends React.PureComponent {
render() {
function sideMenuButton(styles, onPress) {
return (
<TouchableOpacity
onPress={onPress}
accessibilityLabel={_('Sidebar')}
accessibilityHint={_('Show/hide the sidebar')}
accessibilityRole="button">
<TouchableOpacity onPress={onPress}>
<View style={styles.sideMenuButton}>
<Icon name="md-menu" style={styles.topIcon} />
</View>
@@ -240,18 +237,9 @@ class ScreenHeaderComponent extends React.PureComponent {
function backButton(styles, onPress, disabled) {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Back')}
accessibilityHint={_('Navigate to the previous view')}
accessibilityRole="button">
<TouchableOpacity onPress={onPress} disabled={disabled}>
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
<Icon
name="md-arrow-back"
style={styles.topIcon}
/>
<Icon name="md-arrow-back" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
@@ -263,31 +251,20 @@ class ScreenHeaderComponent extends React.PureComponent {
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled}
style={{ padding: 0 }}
accessibilityLabel={_('Save changes')}
accessibilityHint={disabled ? _('Any changes have been saved') : null}
accessibilityRole="button">
<TouchableOpacity onPress={onPress} disabled={disabled} style={{ padding: 0 }}>
<View style={disabled ? styles.saveButtonDisabled : styles.saveButton}>{icon}</View>
</TouchableOpacity>
);
}
const renderTopButton = (options) => {
if (!options.visible) return null;
if (!options.visible || !this.state.showUndoRedoButtons) return null;
const icon = <Icon name={options.iconName} style={this.styles().topIcon} />;
const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton;
return (
<TouchableOpacity
onPress={options.onPress}
style={{ padding: 0 }}
disabled={!!options.disabled}
accessibilityRole="button">
<TouchableOpacity onPress={options.onPress} style={{ padding: 0 }} disabled={!!options.disabled}>
<View style={viewStyle}>{icon}</View>
</TouchableOpacity>
);
@@ -312,11 +289,7 @@ class ScreenHeaderComponent extends React.PureComponent {
function selectAllButton(styles, onPress) {
return (
<TouchableOpacity
onPress={onPress}
accessibilityLabel={_('Select all')}
accessibilityRole="button">
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
</View>
@@ -326,11 +299,7 @@ class ScreenHeaderComponent extends React.PureComponent {
function searchButton(styles, onPress) {
return (
<TouchableOpacity
onPress={onPress}
accessibilityLabel={_('Search')}
accessibilityRole="button">
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
<Icon name="md-search" style={styles.topIcon} />
</View>
@@ -340,15 +309,7 @@ class ScreenHeaderComponent extends React.PureComponent {
function deleteButton(styles, onPress, disabled) {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Delete')}
accessibilityHint={
disabled ? null : _('Delete selected notes')
}
accessibilityRole="button">
<TouchableOpacity onPress={onPress} disabled={disabled}>
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-trash" style={styles.topIcon} />
</View>
@@ -358,15 +319,7 @@ class ScreenHeaderComponent extends React.PureComponent {
function duplicateButton(styles, onPress, disabled) {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Duplicate')}
accessibilityHint={
disabled ? null : _('Duplicate selected notes')
}
accessibilityRole="button">
<TouchableOpacity onPress={onPress} disabled={disabled}>
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-copy" style={styles.topIcon} />
</View>
@@ -376,11 +329,7 @@ class ScreenHeaderComponent extends React.PureComponent {
function sortButton(styles, onPress) {
return (
<TouchableOpacity
onPress={onPress}
accessibilityLabel={_('Sort notes by')}
accessibilityRole="button">
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
<Icon name="filter-outline" style={styles.topIcon} />
</View>
@@ -475,6 +424,16 @@ class ScreenHeaderComponent extends React.PureComponent {
color: theme.color,
fontSize: theme.fontSize,
}}
onOpen={() => {
this.setState({
showUndoRedoButtons: false,
});
}}
onClose={() => {
this.setState({
showUndoRedoButtons: true,
});
}}
onValueChange={async (folderId, itemIndex) => {
// If onValueChange is specified, use this as a callback, otherwise do the default
// which is to take the selectedNoteIds from the state and move them to the

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
import Slider from '@react-native-community/slider';
const React = require('react');
const { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } = require('react-native');
const { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid } = require('react-native');
import Setting, { AppType } from '@joplin/lib/models/Setting';
import NavService from '@joplin/lib/services/NavService';
import ReportService from '@joplin/lib/services/ReportService';
@@ -21,7 +20,6 @@ const { Dropdown } = require('../Dropdown.js');
const { themeStyle } = require('../global-style.js');
const shared = require('@joplin/lib/components/shared/config-shared.js');
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import { openDocumentTree } from '@joplin/react-native-saf-x';
const RNFS = require('react-native-fs');
class ConfigScreenComponent extends BaseScreenComponent {
@@ -39,27 +37,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
creatingReport: false,
profileExportStatus: 'idle',
profileExportPath: '',
fileSystemSyncPath: Setting.value('sync.2.path'),
};
this.scrollViewRef_ = React.createRef();
shared.init(this, reg);
this.selectDirectoryButtonPress = async () => {
try {
const doc = await openDocumentTree(true);
if (doc?.uri) {
this.setState({ fileSystemSyncPath: doc.uri });
shared.updateSettingValue(this, 'sync.2.path', doc.uri);
} else {
throw new Error('User cancelled operation');
}
} catch (e) {
reg.logger().info('Didn\'t pick sync dir: ', e);
}
};
this.checkSyncConfig_ = async () => {
// to ignore TLS erros we need to chage the global state of the app, if the check fails we need to restore the original state
// this call sets the new value and returns the previous one which we can use later to revert the change
@@ -75,15 +58,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
};
this.saveButton_press = async () => {
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem')) {
if (Platform.OS === 'android') {
if (Platform.Version < 29) {
if (!(await this.checkFilesystemPermission())) {
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
}
}
}
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem') && !(await this.checkFilesystemPermission())) {
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
// Save settings anyway, even if permission has not been granted
}
@@ -469,7 +445,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
{descriptionComp}
</View>
);
} else if (md.type === Setting.TYPE_BOOL) {
} else if (md.type == Setting.TYPE_BOOL) {
return this.renderToggle(key, md.label(), value, updateSettingValue, descriptionComp);
// return (
// <View key={key}>
@@ -482,7 +458,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
// {descriptionComp}
// </View>
// );
} else if (md.type === Setting.TYPE_INT) {
} else if (md.type == Setting.TYPE_INT) {
const unitLabel = md.unitLabel ? md.unitLabel(value) : value;
// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
// on the Slider as they are buggy and can crash the app on certain devices.
@@ -499,21 +475,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
</View>
</View>
);
} else if (md.type === Setting.TYPE_STRING) {
if (md.key === 'sync.2.path' && Platform.OS === 'android' && Platform.Version > 28) {
return (
<TouchableNativeFeedback key={key} onPress={this.selectDirectoryButtonPress} style={this.styles().settingContainer}>
<View style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>
{md.label()}
</Text>
<Text style={this.styles().settingControl}>
{this.state.fileSystemSyncPath}
</Text>
</View>
</TouchableNativeFeedback>
);
}
} else if (md.type == Setting.TYPE_STRING) {
return (
<View key={key} style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>

View File

@@ -5,8 +5,7 @@ import shim from '@joplin/lib/shim';
import UndoRedoService from '@joplin/lib/services/UndoRedoService';
import NoteBodyViewer from '../NoteBodyViewer/NoteBodyViewer';
import checkPermissions from '../../utils/checkPermissions';
import NoteEditor from '../NoteEditor/NoteEditor';
import { ChangeEvent, UndoRedoDepthChangeEvent } from '../NoteEditor/types';
import NoteEditor, { ChangeEvent, UndoRedoDepthChangeEvent } from '../NoteEditor/NoteEditor';
const FileViewer = require('react-native-file-viewer').default;
const React = require('react');
@@ -104,8 +103,8 @@ class NoteScreenComponent extends BaseScreenComponent {
if (this.isModified()) {
const buttonId = await dialogs.pop(this, _('This note has been modified:'), [{ text: _('Save changes'), id: 'save' }, { text: _('Discard changes'), id: 'discard' }, { text: _('Cancel'), id: 'cancel' }]);
if (buttonId === 'cancel') return true;
if (buttonId === 'save') await this.saveNoteButton_press();
if (buttonId == 'cancel') return true;
if (buttonId == 'save') await this.saveNoteButton_press();
}
return false;
@@ -127,7 +126,7 @@ class NoteScreenComponent extends BaseScreenComponent {
return false;
}
if (this.state.mode === 'edit') {
if (this.state.mode == 'edit') {
Keyboard.dismiss();
this.setState({
@@ -162,8 +161,8 @@ class NoteScreenComponent extends BaseScreenComponent {
this.onJoplinLinkClick_ = async (msg: string) => {
try {
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
if (resourceUrlInfo) {
if (msg.indexOf('joplin://') === 0) {
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
const itemId = resourceUrlInfo.itemId;
const item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error(_('No item with ID %s', itemId));
@@ -604,7 +603,7 @@ class NoteScreenComponent extends BaseScreenComponent {
reg.logger().info('New dimensions ', dimensions);
const format = mimeType === 'image/png' ? 'PNG' : 'JPEG';
const format = mimeType == 'image/png' ? 'PNG' : 'JPEG';
reg.logger().info(`Resizing image ${localFilePath}`);
const resizedImage = await ImageResizer.createResizedImage(localFilePath, dimensions.width, dimensions.height, format, 85); // , 0, targetPath);
@@ -677,7 +676,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const targetPath = Resource.fullPath(resource);
try {
if (mimeType === 'image/jpeg' || mimeType === 'image/jpg' || mimeType === 'image/png') {
if (mimeType == 'image/jpeg' || mimeType == 'image/jpg' || mimeType == 'image/png') {
const done = await this.resizeImage(localFilePath, targetPath, mimeType);
if (!done) return;
} else {
@@ -712,7 +711,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const newNote = Object.assign({}, this.state.note);
if (this.state.mode === 'edit' && !!this.selection) {
if (this.state.mode == 'edit' && !!this.selection) {
const newText = `\n${resourceTag}\n`;
const prefix = newNote.body.substring(0, this.selection.start);
@@ -1069,7 +1068,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const keywords = this.props.searchQuery && !!this.props.ftsEnabled ? this.props.highlightedWords : emptyArray;
let bodyComponent = null;
if (this.state.mode === 'view') {
if (this.state.mode == 'view') {
// Note: as of 2018-12-29 it's important not to display the viewer if the note body is empty,
// to avoid the HACK_webviewLoadingState related bug.
bodyComponent =
@@ -1155,7 +1154,7 @@ class NoteScreenComponent extends BaseScreenComponent {
},
});
if (this.state.mode === 'edit') return null;
if (this.state.mode == 'edit') return null;
return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} />;
};
@@ -1163,7 +1162,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const actionButtonComp = renderActionButton();
// Save button is not really needed anymore with the improved save logic
const showSaveButton = false; // this.state.mode === 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
const showSaveButton = false; // this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
const saveButtonDisabled = true;// !this.isModified();
if (showSaveButton) this.saveButtonHasBeenShown_ = true;

View File

@@ -83,8 +83,8 @@ class LogScreenComponent extends BaseScreenComponent {
render() {
const renderRow = ({ item }) => {
let textStyle = this.styles().rowText;
if (item.level === Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
if (item.level === Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
if (item.level == Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
if (item.level == Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
return (
<View style={this.styles().row}>

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