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

Compare commits

...

22 Commits

Author SHA1 Message Date
Laurent Cozic
9bba9a8ed6 Plugins: Fixed issue with toolbar button key not being unique 2020-11-20 16:19:57 +00:00
Laurent Cozic
3344e84c7c fix link 2020-11-20 15:52:08 +00:00
Laurent Cozic
e0c2b62a6c Tools: Fixed tests when running them from root 2020-11-20 14:04:02 +00:00
Laurent Cozic
544d879c0b Plugins: Use plugin ID as filename 2020-11-19 23:46:04 +00:00
Laurent Cozic
28f75449d7 Desktop release v1.4.11 2020-11-19 21:02:03 +00:00
Laurent Cozic
fdc84aa6bb Desktop: Upgrade to Electron 10 2020-11-19 21:01:19 +00:00
Laurent Cozic
592b9d95c6 Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-11-19 18:35:56 +00:00
Laurent Cozic
4abdab5cdb Tools: Fixed tests 2020-11-19 18:34:53 +00:00
Caleb John
858508bfa9 Desktop: Add option to toggle spellchecking for the markdown editor (#4109) 2020-11-19 17:14:44 +00:00
Caleb John
116413e78d Desktop: Prevent lines from shifting in Markdown Editor when Scrollbar appears (#4110) 2020-11-19 17:04:18 +00:00
Anshuman Pandey
031a26116c Desktop: Resolves #4096: Disabled the auto update option in linux (#4102) 2020-11-19 17:00:22 +00:00
Laurent Cozic
65962e26ce Desktop: Fixes #4098: Fix crash due to React when trying to upgrade sync target 2020-11-19 16:38:44 +00:00
Laurent Cozic
30913a5d58 API: Always include 'has_more' field for paginated data 2020-11-19 16:25:32 +00:00
Laurent Cozic
61618fb37c All: Refresh sidebar and notes when moving note outside of conflict folder 2020-11-19 16:21:24 +00:00
Laurent Cozic
a40ab434ca Generator: Merge package.json when updating plugin framework 2020-11-19 15:57:47 +00:00
Laurent Cozic
dbbd930f81 All: Added global logger
Avoids having to pass around a logger to every module and sub-module,
and makes it easier to set log level globally.
2020-11-19 15:25:02 +00:00
Laurent Cozic
ff3ae3860e Doc Mentioned gettext version 2020-11-19 13:03:40 +00:00
Laurent Cozic
e1c4d1a417 Doc: Fix link 2020-11-19 13:02:03 +00:00
Laurent Cozic
e1180a1d84 Tools: Remove Jasmine dependency 2020-11-19 12:44:20 +00:00
Laurent Cozic
e57444dc32 Desktop: Add config screen to add, remove or enable, disable plugins 2020-11-19 12:34:49 +00:00
Laurent Cozic
f36019c94d Generator: Fixed webpack config 2020-11-18 12:33:48 +00:00
Laurent Cozic
c67c8cefdf Tools: Update plugin demos 2020-11-18 12:32:58 +00:00
99 changed files with 9630 additions and 1393 deletions

View File

@@ -241,6 +241,15 @@ packages/app-cli/tests/support/plugins/events/api/types.js.map
packages/app-cli/tests/support/plugins/events/src/index.d.ts
packages/app-cli/tests/support/plugins/events/src/index.js
packages/app-cli/tests/support/plugins/events/src/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/index.js
packages/app-cli/tests/support/plugins/jpl_test/api/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/types.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/types.js
packages/app-cli/tests/support/plugins/jpl_test/api/types.js.map
packages/app-cli/tests/support/plugins/jpl_test/src/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/src/index.js
packages/app-cli/tests/support/plugins/jpl_test/src/index.js.map
packages/app-cli/tests/support/plugins/json_export/api/index.d.ts
packages/app-cli/tests/support/plugins/json_export/api/index.js
packages/app-cli/tests/support/plugins/json_export/api/index.js.map
@@ -367,6 +376,9 @@ packages/app-desktop/gui/ConfigScreen/ConfigScreen.js.map
packages/app-desktop/gui/ConfigScreen/SideBar.d.ts
packages/app-desktop/gui/ConfigScreen/SideBar.js
packages/app-desktop/gui/ConfigScreen/SideBar.js.map
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.d.ts
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
@@ -730,6 +742,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js.map
packages/app-desktop/gui/ToolbarButton/styles/index.d.ts
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarButton/styles/index.js.map
packages/app-desktop/gui/dialogs.d.ts
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/dialogs.js.map
packages/app-desktop/gui/hooks/useEffectDebugger.d.ts
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useEffectDebugger.js.map
@@ -742,6 +757,9 @@ packages/app-desktop/gui/hooks/usePrevious.js.map
packages/app-desktop/gui/hooks/usePropsDebugger.d.ts
packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/hooks/usePropsDebugger.js.map
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
packages/app-desktop/gui/menuCommandNames.d.ts
packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map
@@ -1333,6 +1351,9 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/renderer/HtmlToHtml.d.ts
packages/renderer/HtmlToHtml.js
packages/renderer/HtmlToHtml.js.map
packages/renderer/InMemoryCache.d.ts
packages/renderer/InMemoryCache.js
packages/renderer/InMemoryCache.js.map
@@ -1342,6 +1363,12 @@ packages/renderer/MarkupToHtml.js.map
packages/renderer/MdToHtml.d.ts
packages/renderer/MdToHtml.js
packages/renderer/MdToHtml.js.map
packages/renderer/MdToHtml/linkReplacement.d.ts
packages/renderer/MdToHtml/linkReplacement.js
packages/renderer/MdToHtml/linkReplacement.js.map
packages/renderer/MdToHtml/linkReplacement.test.d.ts
packages/renderer/MdToHtml/linkReplacement.test.js
packages/renderer/MdToHtml/linkReplacement.test.js.map
packages/renderer/MdToHtml/rules/checkbox.d.ts
packages/renderer/MdToHtml/rules/checkbox.js
packages/renderer/MdToHtml/rules/checkbox.js.map
@@ -1375,6 +1402,9 @@ packages/renderer/MdToHtml/rules/mermaid.js.map
packages/renderer/MdToHtml/rules/sanitize_html.d.ts
packages/renderer/MdToHtml/rules/sanitize_html.js
packages/renderer/MdToHtml/rules/sanitize_html.js.map
packages/renderer/htmlUtils.d.ts
packages/renderer/htmlUtils.js
packages/renderer/htmlUtils.js.map
packages/renderer/index.d.ts
packages/renderer/index.js
packages/renderer/index.js.map
@@ -1384,4 +1414,7 @@ packages/renderer/noteStyle.js.map
packages/renderer/pathUtils.d.ts
packages/renderer/pathUtils.js
packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@@ -15,7 +15,8 @@ module.exports = {
'Atomics': 'readonly',
'SharedArrayBuffer': 'readonly',
// Jasmine variables
// Jest variables
'test': 'readonly',
'expect': 'readonly',
'describe': 'readonly',
'it': 'readonly',
@@ -23,10 +24,6 @@ module.exports = {
'afterAll': 'readonly',
'beforeEach': 'readonly',
'afterEach': 'readonly',
'jasmine': 'readonly',
// Jest variables
'test': 'readonly',
// React Native variables
'__DEV__': 'readonly',

33
.gitignore vendored
View File

@@ -233,6 +233,15 @@ packages/app-cli/tests/support/plugins/events/api/types.js.map
packages/app-cli/tests/support/plugins/events/src/index.d.ts
packages/app-cli/tests/support/plugins/events/src/index.js
packages/app-cli/tests/support/plugins/events/src/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/index.js
packages/app-cli/tests/support/plugins/jpl_test/api/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/types.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/types.js
packages/app-cli/tests/support/plugins/jpl_test/api/types.js.map
packages/app-cli/tests/support/plugins/jpl_test/src/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/src/index.js
packages/app-cli/tests/support/plugins/jpl_test/src/index.js.map
packages/app-cli/tests/support/plugins/json_export/api/index.d.ts
packages/app-cli/tests/support/plugins/json_export/api/index.js
packages/app-cli/tests/support/plugins/json_export/api/index.js.map
@@ -359,6 +368,9 @@ packages/app-desktop/gui/ConfigScreen/ConfigScreen.js.map
packages/app-desktop/gui/ConfigScreen/SideBar.d.ts
packages/app-desktop/gui/ConfigScreen/SideBar.js
packages/app-desktop/gui/ConfigScreen/SideBar.js.map
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.d.ts
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
@@ -722,6 +734,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js.map
packages/app-desktop/gui/ToolbarButton/styles/index.d.ts
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarButton/styles/index.js.map
packages/app-desktop/gui/dialogs.d.ts
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/dialogs.js.map
packages/app-desktop/gui/hooks/useEffectDebugger.d.ts
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useEffectDebugger.js.map
@@ -734,6 +749,9 @@ packages/app-desktop/gui/hooks/usePrevious.js.map
packages/app-desktop/gui/hooks/usePropsDebugger.d.ts
packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/hooks/usePropsDebugger.js.map
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
packages/app-desktop/gui/menuCommandNames.d.ts
packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map
@@ -1325,6 +1343,9 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/renderer/HtmlToHtml.d.ts
packages/renderer/HtmlToHtml.js
packages/renderer/HtmlToHtml.js.map
packages/renderer/InMemoryCache.d.ts
packages/renderer/InMemoryCache.js
packages/renderer/InMemoryCache.js.map
@@ -1334,6 +1355,12 @@ packages/renderer/MarkupToHtml.js.map
packages/renderer/MdToHtml.d.ts
packages/renderer/MdToHtml.js
packages/renderer/MdToHtml.js.map
packages/renderer/MdToHtml/linkReplacement.d.ts
packages/renderer/MdToHtml/linkReplacement.js
packages/renderer/MdToHtml/linkReplacement.js.map
packages/renderer/MdToHtml/linkReplacement.test.d.ts
packages/renderer/MdToHtml/linkReplacement.test.js
packages/renderer/MdToHtml/linkReplacement.test.js.map
packages/renderer/MdToHtml/rules/checkbox.d.ts
packages/renderer/MdToHtml/rules/checkbox.js
packages/renderer/MdToHtml/rules/checkbox.js.map
@@ -1367,6 +1394,9 @@ packages/renderer/MdToHtml/rules/mermaid.js.map
packages/renderer/MdToHtml/rules/sanitize_html.d.ts
packages/renderer/MdToHtml/rules/sanitize_html.js
packages/renderer/MdToHtml/rules/sanitize_html.js.map
packages/renderer/htmlUtils.d.ts
packages/renderer/htmlUtils.js
packages/renderer/htmlUtils.js.map
packages/renderer/index.d.ts
packages/renderer/index.js
packages/renderer/index.js.map
@@ -1376,4 +1406,7 @@ packages/renderer/noteStyle.js.map
packages/renderer/pathUtils.d.ts
packages/renderer/pathUtils.js
packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@@ -13,6 +13,7 @@
"_vieux/": true,
".gitignore": true,
".eslintignore": true,
"**/*.jpl": true,
"./packages/app-cli/**/*.*~": true,
"./packages/app-cli/**/*.mo": true,
"./packages/app-cli/**/build/": true,
@@ -55,7 +56,6 @@
"./packages/app-desktop/**/*.min.js": true,
"./packages/app-desktop/**/dist/": true,
"./packages/app-desktop/**/gui/note-viewer/pluginAssets/": true,
"./packages/app-desktop/**/lib/": true,
"./packages/app-desktop/**/node_modules/": true,
"./packages/app-desktop/**/packageInfo.js": true,
"./packages/app-desktop/**/pluginAssets/": true,
@@ -242,7 +242,6 @@
"packages/app-desktop/**/*.min.js": true,
"packages/app-desktop/**/dist/": true,
"packages/app-desktop/**/gui/note-viewer/pluginAssets/": true,
"packages/app-desktop/**/lib/": true,
"packages/app-desktop/**/node_modules/": true,
"packages/app-desktop/**/packageInfo.js": true,
"packages/app-desktop/**/pluginAssets/": true,

22
package-lock.json generated
View File

@@ -1227,12 +1227,6 @@
"@types/node": "*"
}
},
"@types/jasmine": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.0.tgz",
"integrity": "sha512-CPT4r0a63e5wpNj5ejMnconM7a+0Hdx6/APsyw8AQOHk0/Mxp3xYrym1ZabWJiYuQkgKB3MonYoN04mxtvAvRA==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
@@ -6439,22 +6433,6 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
"jasmine": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.6.3.tgz",
"integrity": "sha512-Th91zHsbsALWjDUIiU5d/W5zaYQsZFMPTdeNmi8GivZPmAaUAK8MblSG3yQI4VMGC/abF2us7ex60NH1AAIMTA==",
"dev": true,
"requires": {
"glob": "^7.1.6",
"jasmine-core": "~3.6.0"
}
},
"jasmine-core": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz",
"integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@@ -7,6 +7,12 @@
},
"license": "MIT",
"scripts": {
"addPackageCli": "lerna add --scope joplin",
"addPackageCliD": "lerna add --scope joplin -D",
"addPackageDesktop": "lerna add --scope @joplin/app-desktop",
"addPackageDesktopD": "lerna add --scope @joplin/app-desktop -D",
"addPackageMobile": "lerna add --scope @joplin/app-mobile",
"addPackageMobileD": "lerna add --scope @joplin/app-mobile -D",
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
"buildDoc": "./packages/tools/build-all.sh",
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/api/references/plugin_api packages/lib/services/plugins/api/",
@@ -26,11 +32,11 @@
"releaseDesktop": "node packages/tools/release-electron.js",
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
"setupNewRelease": "node ./packages/tools/setupNewRelease",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"tsc": "lerna run tsc --stream --parallel",
"test": "lerna run test --stream",
"test-ci": "lerna run test-ci --stream",
"test": "lerna run test --stream",
"tsc": "lerna run tsc --stream --parallel",
"updateIgnored": "gulp updateIgnoredTypeScriptBuild",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"watch": "lerna run watch --stream --parallel"
},
"husky": {
@@ -39,7 +45,6 @@
}
},
"devDependencies": {
"@types/jasmine": "^3.6.0",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"eslint": "^7.6.0",
@@ -50,7 +55,6 @@
"glob": "^7.1.6",
"gulp": "^4.0.2",
"husky": "^3.0.2",
"jasmine": "^3.5.0",
"lerna": "^3.22.1",
"lint-staged": "^9.2.1",
"typedoc": "^0.17.8",

View File

@@ -426,7 +426,7 @@ class Application extends BaseApplication {
const AppGui = require('./app-gui.js');
this.gui_ = new AppGui(this, this.store(), keymap);
this.gui_.setLogger(this.logger_);
this.gui_.setLogger(this.logger());
await this.gui_.start();
// Since the settings need to be loaded before the store is created, it will never

View File

@@ -30,12 +30,12 @@ module.exports = {
],
testPathIgnorePatterns: [
'/node_modules/',
'/tests\\/support/',
'/build/',
'test-utils.js',
'file_api_driver.js',
'/tests\\/tmp/',
'<rootDir>/node_modules/',
'<rootDir>/tests/support/',
'<rootDir>/build/',
'<rootDir>/tests/test-utils.js',
'<rootDir>/tests/file_api_driver.js',
'<rootDir>/tests/tmp/',
],
// To avoid this warning:
@@ -52,5 +52,5 @@ module.exports = {
],
testEnvironment: 'node',
setupFilesAfterEnv: ['./jest.setup.js'],
setupFilesAfterEnv: [`${__dirname}/jest.setup.js`],
};

View File

@@ -898,11 +898,15 @@
"@types/istanbul-lib-report": "*"
}
},
"@types/jasmine": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.0.tgz",
"integrity": "sha512-CPT4r0a63e5wpNj5ejMnconM7a+0Hdx6/APsyw8AQOHk0/Mxp3xYrym1ZabWJiYuQkgKB3MonYoN04mxtvAvRA==",
"dev": true
"@types/jest": {
"version": "26.0.15",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz",
"integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==",
"dev": true,
"requires": {
"jest-diff": "^26.0.0",
"pretty-format": "^26.0.0"
}
},
"@types/node": {
"version": "14.14.6",
@@ -4511,38 +4515,6 @@
"istanbul-lib-report": "^3.0.0"
}
},
"jasmine": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz",
"integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==",
"dev": true,
"requires": {
"glob": "^7.1.4",
"jasmine-core": "~3.5.0"
},
"dependencies": {
"glob": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz",
"integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"jasmine-core": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
"dev": true
},
"jest": {
"version": "26.6.3",
"resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz",

View File

@@ -66,10 +66,9 @@
},
"devDependencies": {
"@joplin/tools": "^1.0.9",
"@types/jasmine": "^3.6.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"gulp": "^4.0.2",
"jasmine": "^3.5.0",
"jest": "^26.6.3",
"temp": "^0.9.1",
"typescript": "^4.0.5"

View File

@@ -9,7 +9,7 @@ const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
const shim = require('@joplin/lib/shim').default;
const HtmlToHtml = require('@joplin/renderer/HtmlToHtml');
const HtmlToHtml = require('@joplin/renderer/HtmlToHtml').default;
const { enexXmlToMd } = require('@joplin/lib/import-enex-md-gen.js');
process.on('unhandledRejection', (reason, p) => {

View File

@@ -262,14 +262,14 @@ describe('models_Note', function() {
for (const testCase of testCases) {
const [useAbsolutePaths, input, expected] = testCase;
const internalToExternal = await Note.replaceResourceInternalToExternalLinks(input, { useAbsolutePaths });
expect(internalToExternal).toBe(expected, 'replaceResourceInternalToExternalLinks failed');
expect(internalToExternal).toBe(expected);
const externalToInternal = await Note.replaceResourceExternalToInternalLinks(internalToExternal, { useAbsolutePaths });
expect(externalToInternal).toBe(input, 'replaceResourceExternalToInternalLinks failed');
expect(externalToInternal).toBe(input);
}
const result = await Note.replaceResourceExternalToInternalLinks(`[](joplin://${note1.id})`);
expect(result).toBe(`[](:/${note1.id})`, 'replaceResourceExternalToInternalLinks failed (note link)');
expect(result).toBe(`[](:/${note1.id})`);
}));
});

View File

@@ -28,7 +28,7 @@ async function recreateExportDir() {
function fieldsEqual(model1: any, model2: any, fieldNames: string[]) {
for (let i = 0; i < fieldNames.length; i++) {
const f = fieldNames[i];
expect(model1[f]).toBe(model2[f], `For key ${f}`);
expect(model1[f]).toBe(model2[f]);
}
}

View File

@@ -3,8 +3,10 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
import MdToHtml from '@joplin/renderer/MdToHtml';
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js');
const fs = require('fs-extra');
const { asyncTest, expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js');
const Note = require('@joplin/lib/models/Note');
const Folder = require('@joplin/lib/models/Folder');
@@ -43,7 +45,7 @@ describe('services_PluginService', function() {
it('should load and run a simple plugin', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple`]);
await service.loadAndRunPlugins([`${testPluginDir}/simple`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
@@ -59,13 +61,13 @@ describe('services_PluginService', function() {
it('should load and run a simple plugin and handle trailing slash', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple/`]);
await service.loadAndRunPlugins([`${testPluginDir}/simple/`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
}));
it('should load and run a plugin that uses external packages', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`]);
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`], {});
expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError();
const allFolders = await Folder.all();
@@ -78,7 +80,7 @@ describe('services_PluginService', function() {
it('should load multiple plugins from a directory', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`);
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`, {});
const plugin1 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo1');
const plugin2 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo2');
@@ -125,14 +127,14 @@ describe('services_PluginService', function() {
it('should load plugins from JS bundle files', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`);
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`, {});
expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
it('should load plugins from JPL archive', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`]);
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], {});
expect(!!service.pluginById('org.joplinapp.FirstJplPlugin')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
@@ -248,10 +250,35 @@ describe('services_PluginService', function() {
];
for (const testCase of testCases) {
const [appVersion, expected] = testCase;
const plugin = await newPluginService(appVersion as string).loadPluginFromJsBundle('', pluginScript);
expect(plugin.enabled).toBe(expected as boolean);
const [appVersion, hasNoError] = testCase;
const service = newPluginService(appVersion as string);
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
if (hasNoError) {
await expectNotThrow(() => service.runPlugin(plugin));
} else {
await expectThrow(() => service.runPlugin(plugin));
}
}
}));
it('should install a plugin', asyncTest(async () => {
const service = newPluginService();
const pluginPath = `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`;
await service.installPlugin(pluginPath);
const installedPluginPath = `${Setting.value('pluginDir')}/org.joplinapp.FirstJplPlugin.jpl`;
expect(await fs.existsSync(installedPluginPath)).toBe(true);
}));
it('should rename the plugin archive to the right name', asyncTest(async () => {
const tempDir = await createTempDir();
const service = newPluginService();
const pluginPath = `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`;
const tempPath = `${tempDir}/something.jpl`;
await shim.fsDriver().copy(pluginPath, tempPath);
const installedPluginPath = `${Setting.value('pluginDir')}/org.joplinapp.FirstJplPlugin.jpl`;
await service.installPlugin(tempPath);
expect(await fs.existsSync(installedPluginPath)).toBe(true);
}));
});

View File

@@ -3,7 +3,7 @@
const time = require('@joplin/lib/time').default;
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, mockDate, restoreDate } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, restoreDate } = require('./test-utils.js');
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine');
const Note = require('@joplin/lib/models/Note');
const ItemChange = require('@joplin/lib/models/ItemChange');

View File

@@ -23,12 +23,6 @@ let engine = null;
const ids = (array) => array.map(a => a.id);
// For pretty printing.
// See https://stackoverflow.com/questions/23676459/karma-jasmine-pretty-printing-object-comparison/26324116
// jasmine.pp = function(obj) {
// return JSON.stringify(obj, undefined, 2);
// };
describe('services_SearchFilter', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);

View File

@@ -538,7 +538,7 @@ describe('services_rest_Api', function() {
const r3 = await api.route(RequestMethod.GET, 'folders', { ...baseQuery, page: 3 });
expect(r3.items.length).toBe(0);
expect(r3.has_more).toBe(undefined);
expect(r3.has_more).toBe(false);
}
{
@@ -560,7 +560,7 @@ describe('services_rest_Api', function() {
expect(r2.items.length).toBe(1);
expect(r2.items[0].title).toBe('folder4');
expect(r2.has_more).toBe(undefined);
expect(r2.has_more).toBe(false);
}
}));

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -787,11 +793,6 @@
"wrap-ansi": "^5.1.0"
}
},
"codemirror": {
"version": "5.58.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.2.tgz",
"integrity": "sha512-K/hOh24cCwRutd1Mk3uLtjWzNISOkm4fvXiMO7LucCrqbh6aJDdtqUziim3MZUI6wOY0rvY1SlL1Ork01uMy6w=="
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -1617,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2155,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2564,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3788,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1621,6 +1627,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2159,6 +2177,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2575,6 +2611,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3799,6 +3841,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -0,0 +1,17 @@
#!/bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
# cd "$SCRIPT_DIR/jpl_test/" && npm i && \
# cd "$SCRIPT_DIR/codemirror_content_script/" && npm i && \
# cd "$SCRIPT_DIR/content_script/" && npm i && \
# cd "$SCRIPT_DIR/dialog/" && npm i && \
# cd "$SCRIPT_DIR/editor_context_menu/" && npm i && \
# cd "$SCRIPT_DIR/events/" && npm i && \
# cd "$SCRIPT_DIR/json_export/" && npm i && \
# cd "$SCRIPT_DIR/menu/" && npm i && \
# cd "$SCRIPT_DIR/multi_selection/" && npm i && \
# cd "$SCRIPT_DIR/register_command/" && npm i && \
cd "$SCRIPT_DIR/selected_text/" && npm i && \
cd "$SCRIPT_DIR/settings/" && npm i && \
cd "$SCRIPT_DIR/toc/" && npm i && \
cd "$SCRIPT_DIR/withExternalModules/" && npm i

View File

@@ -1,5 +1,5 @@
{
"name": "org.joplinapp.FirstJplPlugin",
"name": "joplin_plugin",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,

View File

@@ -1,5 +1,5 @@
{
"name": "test_plugin",
"name": "joplin_plugin",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -49,6 +49,6 @@ joplin.plugins.register({
},
});
joplin.views.toolbarButtons.create('prettyMarkdownTable', ToolbarButtonLocation.EditorToolbar);
joplin.views.toolbarButtons.create('prettyMarkdownTableButton', 'prettyMarkdownTable', ToolbarButtonLocation.EditorToolbar);
},
});

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2559,6 +2595,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3783,6 +3825,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -37,7 +37,7 @@ joplin.plugins.register({
},
});
await joplin.views.toolbarButtons.create('incValue', ToolbarButtonLocation.NoteToolbar);
await joplin.views.toolbarButtons.create('checkValue', ToolbarButtonLocation.NoteToolbar);
await joplin.views.toolbarButtons.create('incValueButton', 'incValue', ToolbarButtonLocation.NoteToolbar);
await joplin.views.toolbarButtons.create('checkValueButton', 'checkValue', ToolbarButtonLocation.NoteToolbar);
},
});

View File

@@ -1,5 +1,5 @@
{
"name": "test_webview",
"name": "joplin_plugin",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
@@ -407,6 +407,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1873,6 +1879,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2378,6 +2396,24 @@
"minimist": "^1.2.0"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2420,11 +2456,6 @@
"path-exists": "^3.0.0"
}
},
"lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2746,14 +2777,6 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
"integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==",
"requires": {
"lodash.toarray": "^4.4.0"
}
},
"node-libs-browser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@@ -2863,6 +2886,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3989,6 +4018,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unorm": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
@@ -4081,10 +4116,10 @@
"dev": true
},
"uslug": {
"version": "git+https://github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
"from": "git+https://github.com/laurent22/uslug.git#emoji-support",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz",
"integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=",
"requires": {
"node-emoji": "^1.10.0",
"unorm": ">= 1.0.0"
}
},

View File

@@ -19,5 +19,8 @@
"typescript": "^3.9.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
},
"dependencies": {
"uslug": "^1.0.4"
}
}

View File

@@ -400,6 +400,12 @@
"dev": true,
"optional": true
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1612,6 +1618,18 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2150,6 +2168,24 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2564,6 +2600,12 @@
"isobject": "^3.0.1"
}
},
"on-build-webpack": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/on-build-webpack/-/on-build-webpack-0.1.0.tgz",
"integrity": "sha1-oofA4Xdm5hQZJuXyy7DYu1O3aBQ=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3788,6 +3830,12 @@
"imurmurhash": "^0.1.4"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"dev": true
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",

View File

@@ -19,5 +19,8 @@
"typescript": "^3.9.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
},
"dependencies": {
"left-pad": "^1.3.0"
}
}

View File

@@ -131,9 +131,11 @@ setSyncTargetName('memory');
const syncDir = `${__dirname}/../tests/sync`;
let defaultJasmineTimeout = 90 * 1000;
if (isNetworkSyncTarget_) defaultJasmineTimeout = 60 * 1000 * 10;
if (typeof jasmine !== 'undefined') jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultJasmineTimeout;
// TODO: Should probably update this for Jest?
// let defaultJasmineTimeout = 90 * 1000;
// if (isNetworkSyncTarget_) defaultJasmineTimeout = 60 * 1000 * 10;
// if (typeof jasmine !== 'undefined') jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultJasmineTimeout;
const dbLogger = new Logger();
dbLogger.addTarget('console');
@@ -145,6 +147,8 @@ logger.addTarget('console');
logger.addTarget('file', { path: `${logDir}/log.txt` });
logger.setLevel(Logger.LEVEL_WARN); // Set to DEBUG to display sync process in console
Logger.initializeGlobalLogger(logger);
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
@@ -212,6 +216,7 @@ async function switchClient(id, options = null) {
await Setting.reset();
Setting.setConstant('resourceDirName', resourceDirName(id));
Setting.setConstant('resourceDir', resourceDir(id));
Setting.setConstant('pluginDir', pluginDir(id));
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
@@ -290,6 +295,11 @@ function resourceDir(id = null) {
return `${__dirname}/data/${resourceDirName(id)}`;
}
function pluginDir(id = null) {
if (id === null) id = currentClient_;
return `${__dirname}/data/plugins-${id}`;
}
async function setupDatabaseAndSynchronizer(id = null, options = null) {
if (id === null) id = currentClient_;
@@ -303,6 +313,9 @@ async function setupDatabaseAndSynchronizer(id = null, options = null) {
await fs.remove(resourceDir(id));
await fs.mkdirp(resourceDir(id), 0o755);
await fs.remove(pluginDir(id));
await fs.mkdirp(pluginDir(id), 0o755);
if (!synchronizers_[id]) {
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
const syncTarget = new SyncTargetClass(db(id));
@@ -544,7 +557,7 @@ function asyncTest(callback) {
await callback();
} catch (error) {
if (error.constructor && error.constructor.name === 'ExpectationFailed') {
// OK - will be reported by Jasmine
// OK - will be reported by Jest
} else {
// Better to rethrow exception as stack trace is more useful in this case
throw error;
@@ -656,15 +669,17 @@ async function createTempDir() {
return tempDirPath;
}
function mockDate(year, month, day, tick) {
const fixedDate = new Date(2020, 0, 1);
jasmine.clock().install();
jasmine.clock().mockDate(fixedDate);
}
// TODO: Update for Jest
function restoreDate() {
jasmine.clock().uninstall();
}
// function mockDate(year, month, day, tick) {
// const fixedDate = new Date(2020, 0, 1);
// jasmine.clock().install();
// jasmine.clock().mockDate(fixedDate);
// }
// function restoreDate() {
// jasmine.clock().uninstall();
// }
// Application for feature integration testing
class TestApp extends BaseApplication {
@@ -721,7 +736,7 @@ class TestApp extends BaseApplication {
}
async profileDir() {
return await Setting.value('profileDir');
return Setting.value('profileDir');
}
async destroy() {
@@ -734,4 +749,4 @@ class TestApp extends BaseApplication {
}
}
module.exports = { synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, mockDate, restoreDate, TestApp };
module.exports = { synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };

View File

@@ -1,7 +1,6 @@
node_modules/
packageInfo.js
dist/
lib/
*.min.js
.DS_Store
gui/note-viewer/pluginAssets/

View File

@@ -87,6 +87,7 @@ export default class ElectronAppWrapper {
webPreferences: {
nodeIntegration: true,
spellcheck: true,
enableRemoteModule: true,
},
webviewTag: true,
// We start with a hidden window, which is then made visible depending on the showTrayIcon setting

View File

@@ -490,17 +490,24 @@ class Application extends BaseApplication {
}
private async initPluginService() {
const pluginLogger = new Logger();
pluginLogger.addTarget(TargetType.File, { path: `${Setting.value('profileDir')}/log-plugins.txt` });
pluginLogger.addTarget(TargetType.Console, { prefix: 'Plugin Service:' });
pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO);
const service = PluginService.instance();
const pluginRunner = new PluginRunner();
PluginService.instance().setLogger(pluginLogger);
PluginService.instance().initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
// Users can add and remove plugins from the config screen at any
// time, however we only effectively uninstall the plugin the next
// time the app is started. What plugin should be uninstalled is
// stored in the settings.
const newSettings = await service.uninstallPlugins(pluginSettings);
Setting.setValue('plugins.states', newSettings);
try {
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir'));
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
}
@@ -508,12 +515,12 @@ class Application extends BaseApplication {
try {
if (Setting.value('plugins.devPluginPaths')) {
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
await PluginService.instance().loadAndRunPlugins(paths);
await service.loadAndRunPlugins(paths, pluginSettings, true);
}
// Also load dev plugins that have passed via command line arguments
if (Setting.value('startupDevPlugins')) {
await PluginService.instance().loadAndRunPlugins(Setting.value('startupDevPlugins'));
await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, true);
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
@@ -723,6 +730,14 @@ class Application extends BaseApplication {
// console.info(CommandService.instance().commandsToMarkdownTable(this.store().getState()));
// }, 2000);
// this.dispatch({
// type: 'NAV_GO',
// routeName: 'Config',
// props: {
// defaultSection: 'plugins',
// },
// });
return null;
}

View File

@@ -87,7 +87,7 @@ export class Bridge {
return filePath;
}
showOpenDialog(options: any) {
showOpenDialog(options: any = null) {
const { dialog } = require('electron');
if (!options) options = {};
let fileType = 'file';
@@ -117,13 +117,16 @@ export class Bridge {
}
showConfirmMessageBox(message: string, options: any = null) {
if (options === null) options = {};
options = {
buttons: [_('OK'), _('Cancel')],
...options,
};
const result = this.showMessageBox_(this.window(), Object.assign({}, {
type: 'question',
message: message,
cancelId: 1,
buttons: [_('OK'), _('Cancel')],
buttons: options.buttons,
}, options));
return result === 0;
@@ -167,8 +170,8 @@ export class Bridge {
return require('electron').shell.openExternal(url);
}
openItem(fullPath: string) {
return require('electron').shell.openItem(fullPath);
async openItem(fullPath: string) {
return require('electron').shell.openPath(fullPath);
}
checkForUpdates(inBackground: boolean, window: any, logFilePath: string, options: any) {

View File

@@ -3,17 +3,23 @@ import SideBar from './SideBar';
import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button';
import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting from '@joplin/lib/models/Setting';
import control_PluginsStates from './controls/PluginsStates';
const { connect } = require('react-redux');
const Setting = require('@joplin/lib/models/Setting').default;
const { themeStyle } = require('@joplin/lib/theme');
const pathUtils = require('@joplin/lib/path-utils');
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry');
const shared = require('@joplin/lib/components/shared/config-shared.js');
const bridge = require('electron').remote.require('./bridge').default;
const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('../ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
const settingKeyToControl: any = {
'plugins.states': control_PluginsStates,
};
class ConfigScreenComponent extends React.Component<any, any> {
rowStyle_: any = null;
@@ -27,6 +33,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
selectedSectionName: 'general',
screenName: '',
changedSettingKeys: [],
needRestart: false,
};
this.rowStyle_ = {
@@ -41,6 +48,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.onCancelClick = this.onCancelClick.bind(this);
this.onSaveClick = this.onSaveClick.bind(this);
this.onApplyClick = this.onApplyClick.bind(this);
this.renderLabel = this.renderLabel.bind(this);
this.renderDescription = this.renderDescription.bind(this);
}
async checkSyncConfig_() {
@@ -261,6 +270,40 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
private labelStyle(themeId: number) {
const theme = themeStyle(themeId);
return Object.assign({}, theme.textStyle, {
display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 4,
});
}
private descriptionStyle(themeId: number) {
const theme = themeStyle(themeId);
return Object.assign({}, theme.textStyle, {
color: theme.colorFaded,
fontStyle: 'italic',
maxWidth: '70em',
marginTop: 5,
});
}
private renderLabel(themeId: number, label: string) {
const labelStyle = this.labelStyle(themeId);
return (
<div style={labelStyle}>
<label>{label}</label>
</div>
);
}
private renderDescription(themeId: number, description: string) {
return description ? <div style={this.descriptionStyle(themeId)}>{description}</div> : null;
}
settingToComponent(key: string, value: any) {
const theme = themeStyle(this.props.themeId);
@@ -270,13 +313,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
marginBottom: theme.mainPadding,
};
const labelStyle = Object.assign({}, theme.textStyle, {
display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 4,
});
const labelStyle = this.labelStyle(this.props.themeId);
const subLabel = Object.assign({}, labelStyle, {
display: 'block',
@@ -297,13 +334,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
backgroundColor: theme.backgroundColor,
};
const descriptionStyle = Object.assign({}, theme.textStyle, {
color: theme.colorFaded,
marginTop: 5,
fontStyle: 'italic',
maxWidth: '70em',
});
const textInputBaseStyle = Object.assign({}, controlStyle, {
fontFamily: theme.fontFamily,
border: '1px solid',
@@ -318,18 +348,39 @@ class ConfigScreenComponent extends React.Component<any, any> {
});
const updateSettingValue = (key: string, value: any) => {
// console.info(key + ' = ' + value);
return shared.updateSettingValue(this, key, value);
};
const md = Setting.settingMetadata(key);
if (md.needRestart) {
this.setState({ needRestart: true });
}
shared.updateSettingValue(this, key, value);
// Component key needs to be key+value otherwise it doesn't update when the settings change.
if (md.autoSave) {
shared.scheduleSaveSettings(this);
}
};
const md = Setting.settingMetadata(key);
const descriptionText = Setting.keyDescription(key, 'desktop');
const descriptionComp = descriptionText ? <div style={descriptionStyle}>{descriptionText}</div> : null;
const descriptionComp = this.renderDescription(this.props.themeId, descriptionText);
if (md.isEnum) {
if (settingKeyToControl[key]) {
const SettingComponent = settingKeyToControl[key];
return (
<div key={key} style={rowStyle}>
{this.renderLabel(this.props.themeId, md.label())}
{this.renderDescription(this.props.themeId, md.description ? md.description() : null)}
<SettingComponent
metadata={md}
value={value}
themeId={this.props.themeId}
onChange={(event: any) => {
updateSettingValue(key, event.value);
}}
/>
</div>
);
} else if (md.isEnum) {
const items = [];
const settingOptions = md.options();
const array = this.keyValueToArray(settingOptions);
@@ -568,12 +619,33 @@ class ConfigScreenComponent extends React.Component<any, any> {
return output;
}
onApplyClick() {
shared.saveSettings(this);
private restartMessage() {
return _('The application must be restarted for these changes to take effect.');
}
onSaveClick() {
private async restartApp() {
await Setting.saveAll();
bridge().restart();
}
private async checkNeedRestart() {
if (this.state.needRestart) {
const doItNow = await bridge().showConfirmMessageBox(this.restartMessage(), {
buttons: [_('Do it now'), _('Later')],
});
if (doItNow) await this.restartApp();
}
}
async onApplyClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
}
async onSaveClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
this.props.dispatch({ type: 'NAV_BACK' });
}
@@ -621,6 +693,13 @@ class ConfigScreenComponent extends React.Component<any, any> {
const sections = shared.settingsSections({ device: 'desktop', settings });
const needRestartComp: any = this.state.needRestart ? (
<div style={{ ...theme.textStyle, padding: 10, paddingLeft: 24, backgroundColor: theme.warningBackgroundColor, color: theme.color }}>
{this.restartMessage()}
<a style={{ ...theme.urlStyle, marginLeft: 10 }} href="#" onClick={() => { this.restartApp(); }}>{_('Restart now')}</a>
</div>
) : null;
return (
<div style={{ display: 'flex', flexDirection: 'row' }}>
<SideBar
@@ -630,6 +709,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
/>
<div style={style}>
{screenComp}
{needRestartComp}
<div style={containerStyle}>{settingComps}</div>
<ButtonBar
hasChanges={hasChanges}

View File

@@ -0,0 +1,255 @@
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { _ } from '@joplin/lib/locale';
import styled from 'styled-components';
import ToggleButton from '../../lib/ToggleButton/ToggleButton';
import Button, { ButtonLevel } from '../../Button/Button';
import bridge from '../../../services/bridge';
import produce from 'immer';
const Root = styled.div`
display: flex;
flex-direction: column;
`;
const TableRoot = styled.div`
display: flex;
flex-wrap: wrap;
`;
const InstallButton = styled(Button)`
margin-bottom: 10px;
`;
const CellRoot = styled.div`
display: flex;
background-color: ${props => props.theme.backgroundColor};
flex-direction: column;
align-items: flex-start;
padding: 15px;
border: 1px solid ${props => props.theme.dividerColor};
border-radius: 6px;
width: 250px;
margin-right: 20px;
margin-bottom: 20px;
box-shadow: 1px 1px 3px rgba(0,0,0,0.2);
`;
const CellTop = styled.div`
display: flex;
flex-direction: row;
width: 100%;
margin-bottom: 10px;
`;
const CellContent = styled.div`
display: flex;
margin-bottom: 10px;
flex: 1;
`;
const CellFooter = styled.div`
display: flex;
flex-direction: row;
`;
const DevModeLabel = styled.div`
border: 1px solid ${props => props.theme.color};
border-radius: 4px;
padding: 4px 6px;
font-size: ${props => props.theme.fontSize * 0.75}px;
color: ${props => props.theme.color};
`;
const StyledName = styled.div`
font-family: ${props => props.theme.fontFamily};
color: ${props => props.theme.color};
font-size: ${props => props.theme.fontSize}px;
font-weight: bold;
flex: 1;
`;
const StyledDescription = styled.div`
font-family: ${props => props.theme.fontFamily};
color: ${props => props.theme.colorFaded};
font-size: ${props => props.theme.fontSize}px;
line-height: 1.6em;
`;
interface Props {
value: any;
themeId: number;
onChange: Function;
}
interface CellProps {
item: PluginItem;
themeId: number;
onToggle: Function;
onDelete: Function;
}
interface PluginItem {
id: string;
name: string;
description: string;
enabled: boolean;
deleted: boolean;
devMode: boolean;
}
function Cell(props: CellProps) {
const { item } = props;
// For plugins in dev mode things like enabling/disabling or
// uninstalling them doesn't make sense, as that should be done by
// adding/removing them from wherever they were loaded from.
function renderToggleButton() {
if (item.devMode) {
return <DevModeLabel>DEV</DevModeLabel>;
}
return <ToggleButton
themeId={props.themeId}
value={item.enabled}
onToggle={() => props.onToggle({ item: props.item })}
/>;
}
function renderFooter() {
if (item.devMode) return null;
return (
<CellFooter>
<Button level={ButtonLevel.Secondary} onClick={() => props.onDelete({ item: props.item })} title={_('Delete')}/>
<div style={{ display: 'flex', flex: 1 }}/>
</CellFooter>
);
}
return (
<CellRoot>
<CellTop>
<StyledName mb={'5px'}>{item.name} {item.deleted ? '(Deleted)' : ''}</StyledName>
{renderToggleButton()}
</CellTop>
<CellContent>
<StyledDescription>{item.description}</StyledDescription>
</CellContent>
{renderFooter()}
</CellRoot>
);
}
function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[] {
return useMemo(() => {
const output: PluginItem[] = [];
for (const pluginId in plugins) {
const plugin = plugins[pluginId];
const setting: PluginSetting = {
...defaultPluginSetting(),
...settings[pluginId],
};
output.push({
id: pluginId,
name: plugin.manifest.name,
description: plugin.manifest.description,
enabled: setting.enabled,
deleted: setting.deleted,
devMode: plugin.devMode,
});
}
output.sort((a: PluginItem, b: PluginItem) => {
return a.name < b.name ? -1 : +1;
});
return output;
}, [plugins, settings]);
}
export default function(props: Props) {
const pluginService = PluginService.instance();
const pluginSettings = useMemo(() => {
return pluginService.unserializePluginSettings(props.value);
}, [props.value]);
const onDelete = useCallback(async (event: any) => {
const item: PluginItem = event.item;
const confirm = await bridge().showConfirmMessageBox(_('Delete plugin "%s"?', item.name));
if (!confirm) return;
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
if (!draft[item.id]) draft[item.id] = defaultPluginSetting();
draft[item.id].deleted = true;
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
}, [pluginSettings, props.onChange]);
const onToggle = useCallback((event: any) => {
const item: PluginItem = event.item;
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
if (!draft[item.id]) draft[item.id] = defaultPluginSetting();
draft[item.id].enabled = !draft[item.id].enabled;
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
}, [pluginSettings, props.onChange]);
const onInstall = useCallback(async () => {
const result = bridge().showOpenDialog({
filters: [{ name: 'Joplin Plugin Archive', extensions: ['jpl'] }],
});
const filePath = result && result.length ? result[0] : null;
if (!filePath) return;
const plugin = await pluginService.installPlugin(filePath);
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
draft[plugin.manifest.id] = defaultPluginSetting();
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
}, [pluginSettings, props.onChange]);
function renderCells(items: PluginItem[]) {
const output = [];
for (const item of items) {
if (item.deleted) continue;
output.push(<Cell
key={item.id}
item={item}
themeId={props.themeId}
onDelete={onDelete}
onToggle={onToggle}
/>);
}
return output;
}
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
return (
<Root>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<InstallButton level={ButtonLevel.Primary} onClick={onInstall} title={_('Install plugin')}/>
<div style={{ display: 'flex', flex: 1 }}/>
</div>
<TableRoot>
{renderCells(pluginItems)}
</TableRoot>
</Root>
);
}

View File

@@ -6,7 +6,7 @@ const { themeStyle } = require('@joplin/lib/theme');
const { _ } = require('@joplin/lib/locale');
const time = require('@joplin/lib/time').default;
const shim = require('@joplin/lib/shim').default;
const dialogs = require('./dialogs');
const dialogs = require('./dialogs').default;
const shared = require('@joplin/lib/components/shared/encryption-config-shared.js');
const bridge = require('electron').remote.require('./bridge').default;

View File

@@ -16,12 +16,12 @@ import { MenuItem, MenuItemLocation } from '@joplin/lib/services/plugins/api/typ
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import menuCommandNames from './menuCommandNames';
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
import bridge from '../services/bridge';
const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js');
const packageInfo = require('../packageInfo.js');
const bridge = require('electron').remote.require('./bridge').default;
const { shell, clipboard } = require('electron');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
const PluginManager = require('@joplin/lib/services/PluginManager');
const TemplateUtils = require('@joplin/lib/TemplateUtils');
@@ -300,7 +300,7 @@ function useMenu(props: Props) {
}, {
label: _('Open template directory'),
click: () => {
shell.openItem(Setting.value('templateDir'));
bridge().openItem(Setting.value('templateDir'));
},
}, {
label: _('Refresh templates'),

View File

@@ -22,6 +22,8 @@ import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import { themeStyle } from '@joplin/lib/theme';
import { ThemeAppearance } from '@joplin/lib/themes/type';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import dialogs from '../../../dialogs';
const Note = require('@joplin/lib/models/Note.js');
const { clipboard } = require('electron');
@@ -29,7 +31,6 @@ const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { reg } = require('@joplin/lib/registry.js');
const dialogs = require('../../../dialogs');
const menuUtils = new MenuUtils(CommandService.instance());
@@ -255,54 +256,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
}
}, []);
const onEditorContextMenu = useCallback(() => {
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
const clipboardText = clipboard.readText();
menu.append(
new MenuItem({
label: _('Cut'),
enabled: hasSelectedText,
click: async () => {
editorCutText();
},
})
);
menu.append(
new MenuItem({
label: _('Copy'),
enabled: hasSelectedText,
click: async () => {
editorCopyText();
},
})
);
menu.append(
new MenuItem({
label: _('Paste'),
enabled: true,
click: async () => {
if (clipboardText) {
editorPasteText();
} else {
// To handle pasting images
onEditorPaste();
}
},
})
);
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
menu.append(new MenuItem(item));
});
menu.popup(bridge().window());
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste, props.plugins]);
const loadScript = async (script: any) => {
return new Promise((resolve) => {
let element: any = document.createElement('script');
@@ -409,6 +362,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
padding-bottom: 400px !important;
}
.CodeMirror-sizer {
/* Add a fixed right padding to account for the appearance (and disappearance) */
/* of the sidebar */
padding-right: 10px !important;
}
.cm-header-1 {
font-size: 1.5em;
}
@@ -623,6 +582,91 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
editorRef.current.refresh();
}, [rootSize, styles.editor, props.visiblePanes]);
// The below code adds support for spellchecking when it is enabled
// It might be buggy, refer to the below issue
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
useEffect(() => {
function pointerInsideEditor(x: number, y: number) {
const elements = document.getElementsByClassName('codeMirrorEditor');
if (!elements.length) return null;
const rect = elements[0].getBoundingClientRect();
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
}
function onContextMenu(_event: any, params: any) {
if (!pointerInsideEditor(params.x, params.y)) return;
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
const clipboardText = clipboard.readText();
menu.append(
new MenuItem({
label: _('Cut'),
enabled: hasSelectedText,
click: async () => {
editorCutText();
},
})
);
menu.append(
new MenuItem({
label: _('Copy'),
enabled: hasSelectedText,
click: async () => {
editorCopyText();
},
})
);
menu.append(
new MenuItem({
label: _('Paste'),
enabled: true,
click: async () => {
if (clipboardText) {
editorPasteText();
} else {
// To handle pasting images
onEditorPaste();
}
},
})
);
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {
menu.append(new MenuItem(item));
}
// Typically CodeMirror handles all interactions itself (highlighting etc.)
// But in the case of clicking a mispelled word, we need electron to handle the click
// The result is that CodeMirror doesn't know what's been selected and doesn't
// move the cursor into the correct location.
// and when the user selects a new spelling it will be inserted in the wrong location
// So in this situation, we use must manually align the internal codemirror selection
// to the contextmenu selection
if (editorRef.current && spellCheckerMenuItems.length > 0) {
editorRef.current.alignSelection(params);
}
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
menu.append(new MenuItem(item));
});
menu.popup();
}
bridge().window().webContents.on('context-menu', onContextMenu);
return () => {
bridge().window().webContents.off('context-menu', onContextMenu);
};
}, []);
function renderEditor() {
return (
@@ -640,7 +684,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
plugins={props.plugins}
onChange={codeMirror_change}
onScroll={editor_scroll}
onEditorContextMenu={onEditorContextMenu}
onEditorPaste={onEditorPaste}
/>
</div>

View File

@@ -27,6 +27,8 @@ import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown
import 'codemirror/mode/meta';
import Setting from '@joplin/lib/models/Setting';
// import eventManager from '@joplin/lib/eventManager';
const { reg } = require('@joplin/lib/registry.js');
@@ -77,19 +79,18 @@ for (let i = 0; i < topLanguages.length; i++) {
}
export interface EditorProps {
value: string,
searchMarkers: any,
mode: string,
style: any,
codeMirrorTheme: any,
readOnly: boolean,
autoMatchBraces: boolean,
keyMap: string,
plugins: PluginStates,
onChange: any,
onScroll: any,
onEditorContextMenu: any,
onEditorPaste: any,
value: string;
searchMarkers: any;
mode: string;
style: any;
codeMirrorTheme: any;
readOnly: boolean;
autoMatchBraces: boolean;
keyMap: string;
plugins: PluginStates;
onChange: any;
onScroll: any;
onEditorPaste: any;
}
function Editor(props: EditorProps, ref: any) {
@@ -122,13 +123,6 @@ function Editor(props: EditorProps, ref: any) {
props.onScroll();
}, [props.onScroll]);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const editor_mousedown = useCallback((_cm: any, event: any) => {
if (event && event.button === 2) {
props.onEditorContextMenu();
}
}, [props.onEditorContextMenu]);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const editor_paste = useCallback((_cm: any, _event: any) => {
props.onEditorPaste();
@@ -163,7 +157,7 @@ function Editor(props: EditorProps, ref: any) {
mode: props.mode,
readOnly: props.readOnly,
autoCloseBrackets: props.autoMatchBraces,
inputStyle: 'textarea', // contenteditable loses cursor position on focus change, use textarea instead
inputStyle: Setting.value('editor.spellcheckBeta') ? 'contenteditable' : 'textarea',
lineWrapping: true,
lineNumbers: false,
indentWithTabs: true,
@@ -177,7 +171,6 @@ function Editor(props: EditorProps, ref: any) {
setEditor(cm);
cm.on('change', editor_change);
cm.on('scroll', editor_scroll);
cm.on('mousedown', editor_mousedown);
cm.on('paste', editor_paste);
cm.on('drop', editor_drop);
cm.on('dragover', editor_drag);
@@ -191,7 +184,6 @@ function Editor(props: EditorProps, ref: any) {
// Clean up codemirror
cm.off('change', editor_change);
cm.off('scroll', editor_scroll);
cm.off('mousedown', editor_mousedown);
cm.off('paste', editor_paste);
cm.off('drop', editor_drop);
cm.off('dragover', editor_drag);
@@ -250,7 +242,7 @@ function Editor(props: EditorProps, ref: any) {
}
}, [pluginOptions, editor]);
return <div style={props.style} ref={editorParent} />;
return <div className='codeMirrorEditor' style={props.style} ref={editorParent} />;
}
export default forwardRef(Editor);

View File

@@ -100,5 +100,33 @@ export default function useCursorUtils(CodeMirror: any) {
});
});
// params are the oncontextmenu params
CodeMirror.defineExtension('alignSelection', function(params: any) {
// The below is a HACK that uses the selectionText from electron and the coordinates of
// the click to determine what the codemirror selection should be
const alignStrings = (s1: string, s2: string) => {
for (let i = 0; i < s1.length; i++) {
if (s1.substr(i, s2.length) === s2) { return i; }
}
return -1;
};
const selectionText = params.selectionText;
const coords = this.coordsChar({ left: params.x, top: params.y });
const { anchor, head } = this.findWordAt(coords);
const selectedWord = this.getRange(anchor, head);
if (selectionText.length > selectedWord.length) {
const offset = alignStrings(selectionText, selectedWord);
anchor.ch -= offset;
head.ch = anchor.ch + selectionText.length;
} else if (selectionText.length < selectedWord.length) {
const offset = alignStrings(selectedWord, selectionText);
anchor.ch += offset;
head.ch = anchor.ch + selectionText.length;
}
this.setSelection(anchor, head);
});
}

View File

@@ -4,6 +4,7 @@ const { buildStyle } = require('@joplin/lib/theme');
export default function styles(props: NoteBodyEditorProps) {
return buildStyle(['TinyMCE', props.style.width, props.style.height], props.themeId, (theme: any) => {
const extraToolbarContainer = {
boxSizing: 'content-box',
backgroundColor: theme.backgroundColor3,
display: 'flex',
flexDirection: 'row',

View File

@@ -3,15 +3,16 @@ import { FormNote, defaultFormNote, ResourceInfos } from './types';
import { clearResourceCache, attachedResources } from './resourceHandling';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import { handleResourceDownloadMode } from './resourceHandling';
import HtmlToHtml from '@joplin/renderer/HtmlToHtml';
import Setting from '@joplin/lib/models/Setting';
import usePrevious from '../../hooks/usePrevious';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
const { MarkupToHtml } = require('@joplin/renderer');
const HtmlToHtml = require('@joplin/renderer/HtmlToHtml');
const usePrevious = require('../../hooks/usePrevious').default;
const Note = require('@joplin/lib/models/Note');
const Setting = require('@joplin/lib/models/Setting').default;
const { reg } = require('@joplin/lib/registry.js');
const ResourceFetcher = require('@joplin/lib/services/ResourceFetcher.js');
const DecryptionWorker = require('@joplin/lib/services/DecryptionWorker.js');
const ResourceEditWatcher = require('@joplin/lib/services/ResourceEditWatcher/index').default;
export interface OnLoadEvent {
formNote: FormNote;

View File

@@ -15,7 +15,6 @@ const { render } = require('react-dom');
const { connect, Provider } = require('react-redux');
const Setting = require('@joplin/lib/models/Setting').default;
const shim = require('@joplin/lib/shim').default;
shim.setReact(React);
const { ImportScreen } = require('./ImportScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
const { Navigator } = require('./Navigator.min.js');
@@ -32,6 +31,10 @@ interface Props {
}
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
div, span, a {
/*color: ${(props: any) => props.theme.color};*/
/*font-size: ${(props: any) => props.theme.fontSize}px;*/

View File

@@ -41,14 +41,14 @@ export default function styles(props: Props) {
leftIcon: {
fontSize: iconSize,
position: 'relative',
top: 2,
top: 1,
color: theme.color3,
},
rightIcon: {
fontSize: iconSize - 1,
borderLeft: 'none',
position: 'relative',
top: 2,
top: 1,
color: theme.color3,
},
};

View File

@@ -40,6 +40,7 @@ class ToolbarBaseComponent extends React.Component<Props, any> {
const o = this.props.items[i];
let key = o.iconName ? o.iconName : '';
key += o.title ? o.title : '';
key += o.name ? o.name : '';
const itemType = !('type' in o) ? 'button' : o.type;
if (!key) key = `${o.type}_${i}`;

View File

@@ -1,20 +1,20 @@
const smalltalk = require('smalltalk');
class Dialogs {
async alert(message, title = '') {
async alert(message: string, title = '') {
await smalltalk.alert(title, message);
}
async confirm(message, title = '') {
async confirm(message: string, title = '', options: any = {}) {
try {
await smalltalk.confirm(title, message);
await smalltalk.confirm(title, message, options);
return true;
} catch (error) {
return false;
}
}
async prompt(message, title = '', defaultValue = '', options = null) {
async prompt(message: string, title = '', defaultValue = '', options: any = null) {
if (options === null) options = {};
try {
@@ -28,4 +28,4 @@ class Dialogs {
const dialogs = new Dialogs();
module.exports = dialogs;
export default dialogs;

View File

@@ -0,0 +1,37 @@
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
const ReactToggleButton = require('react-toggle-button');
const Color = require('color');
interface Props {
value: boolean;
onToggle: Function;
themeId: number;
}
export default function(props: Props) {
const theme = themeStyle(props.themeId);
return (
<ReactToggleButton
value={props.value}
onToggle={props.onToggle}
colors={{
activeThumb: {
base: Color(theme.color5).rgb().string(),
},
active: {
base: Color(theme.backgroundColor5).alpha(0.7).rgb().string(),
},
}}
trackStyle={{
opacity: props.value ? 1 : 0.3,
}}
thumbStyle={{
opacity: props.value ? 1 : 0.5,
}}
inactiveLabel=""
activeLabel=""
/>
);
}

View File

@@ -27,6 +27,7 @@ const { shimInit } = require('@joplin/lib/shim-init-node.js');
const EncryptionService = require('@joplin/lib/services/EncryptionService');
const bridge = require('electron').remote.require('./bridge').default;
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const React = require('react');
if (bridge().env() === 'dev') {
const newConsole = function(oldConsole) {
@@ -86,7 +87,7 @@ try {
keytar = null;
}
shimInit(null, keytar);
shimInit(null, keytar, React);
// Disable drag and drop of links inside application (which would
// open it as if the whole app was a browser)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.4.10",
"version": "1.4.11",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -11,7 +11,8 @@
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json",
"start": "gulp build && electron . --env dev --log-level debug --no-welcome --open-dev-tools",
"test": "jest"
"test": "jest",
"test-ci": "test"
},
"repository": {
"type": "git",
@@ -89,7 +90,6 @@
"devDependencies": {
"@joplin/tools": "^1.0.9",
"@testing-library/react-hooks": "^3.4.2",
"@types/jasmine": "^3.5.11",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/react": "16.9.55",
@@ -98,9 +98,9 @@
"app-builder-bin": "^1.9.11",
"babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1",
"electron": "^8.2.5",
"electron-builder": "22.3.2",
"electron-rebuild": "^1.10.1",
"electron": "^10.1.6",
"electron-builder": "22.9.1",
"electron-rebuild": "^2.3.2",
"glob": "^7.1.6",
"gulp": "^4.0.2",
"jest": "^26.6.3",
@@ -144,6 +144,7 @@
"react-dom": "16.9.0",
"react-redux": "^5.0.7",
"react-select": "^2.4.3",
"react-toggle-button": "^2.2.0",
"react-tooltip": "^3.10.0",
"redux": "^3.7.2",
"reselect": "^4.0.0",

View File

@@ -100,9 +100,11 @@ export default class PluginRunner extends BasePluginRunner {
slashes: true,
})}?pluginId=${encodeURIComponent(plugin.id)}&pluginScript=${encodeURIComponent(`file://${scriptPath}`)}`);
pluginWindow.webContents.once('dom-ready', () => {
pluginWindow.webContents.openDevTools({ mode: 'detach' });
});
if (plugin.devMode) {
pluginWindow.webContents.once('dom-ready', () => {
pluginWindow.webContents.openDevTools({ mode: 'detach' });
});
}
ipcRenderer.on('pluginMessage', async (_event: any, message: PluginMessage) => {
if (message.target !== PluginMessageTarget.MainWindow) return;

View File

@@ -87,7 +87,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
codeTheme: theme.codeThemeCss,
postMessageSyntax: 'window.joplinPostMessage_',
enableLongPress: shim.mobilePlatform() === 'android', // On iOS, there's already a built-on open/share menu
longPressDelay: 500, // TODO use system value
};
// Whenever a resource state changes, for example when it goes from "not downloaded" to "downloaded", the "noteResources"

View File

@@ -399,6 +399,8 @@ async function initialize(dispatch) {
mainLogger.setLevel(Logger.LEVEL_DEBUG);
}
Logger.initializeGlobalLogger(mainLogger);
reg.setLogger(mainLogger);
reg.setShowErrorMessageBoxHandler((message) => { alert(message); });

View File

@@ -4,6 +4,29 @@ const Generator = require('yeoman-generator');
const chalk = require('chalk');
const yosay = require('yosay');
function mergePackageKey(parentKey, source, dest) {
const output = Object.assign({}, dest);
for (const k in source) {
if (!(k in output)) {
// If the key doesn't exist in the destination, add it
output[k] = source[k];
} else if (parentKey === 'devDependencies') {
// If we are dealing with the dependencies, overwrite with the
// version from source.
output[k] = source[k];
} else if (typeof source[k] === 'object' && !Array.isArray(k) && source[k] !== null) {
// If it's an object, recursively process it
output[k] = mergePackageKey(k, source[k], output[k]);
} else {
// Otherwise, the default is to preserve the destination key
output[k] = dest[k];
}
}
return output;
}
module.exports = class extends Generator {
constructor(args, opts) {
@@ -18,8 +41,6 @@ module.exports = class extends Generator {
}
}
async prompting() {
this.log(yosay(`Welcome to the fine ${chalk.red('generator-joplin')} generator!`));
@@ -110,12 +131,28 @@ module.exports = class extends Generator {
if (this.options.update && noUpdateFiles.includes(file)) continue;
const destFile = file.replace(/_TEMPLATE/, '');
const destFilePath = this.destinationPath(destFile);
this.fs.copyTpl(
this.templatePath(file),
this.destinationPath(destFile),
this.props
);
if (this.options.update && destFile === 'package.json' && this.fs.exists(destFilePath)) {
const destContent = this.fs.readJSON(destFilePath);
this.fs.copy(
this.templatePath(file),
destFilePath, {
process: (sourceBuffer) => {
const sourceContent = JSON.parse(sourceBuffer.toString());
const newContent = mergePackageKey(null, sourceContent, destContent);
return JSON.stringify(newContent, null, 2);
},
}
);
} else {
this.fs.copyTpl(
this.templatePath(file),
destFilePath,
this.props
);
}
}
this.fs.copy(

View File

@@ -16,6 +16,13 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`)
.map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) {
// Usually means there's an error, which is going to be printed by
// webpack
console.info('Plugin archive was not created because the "dist" directory is empty');
return;
}
fs.removeSync(destPath);
tar.create(
@@ -38,6 +45,8 @@ const manifestPath = `${srcDir}/manifest.json`;
const manifest = readManifest(manifestPath);
const archiveFilePath = path.resolve(__dirname, `${manifest.id}.jpl`);
fs.removeSync(distDir);
module.exports = {
mode: 'production',
entry: './src/index.ts',

View File

@@ -46,13 +46,13 @@ const MigrationService = require('./services/MigrationService');
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');
const appLogger = Logger.create('App');
// const ntpClient = require('./vendor/ntp-client');
// ntpClient.dgram = require('dgram');
export default class BaseApplication {
private logger_: Logger;
private dbLogger_: Logger;
private eventEmitter_: any;
private scheduleAutoAddResourcesIID_: any = null;
private database_: any = null;
@@ -68,10 +68,7 @@ export default class BaseApplication {
protected store_: any = null;
constructor() {
this.logger_ = new Logger();
this.dbLogger_ = new Logger();
this.eventEmitter_ = new EventEmitter();
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = this.decryptionWorker_resourceMetadataButNotBlobDecrypted.bind(this);
}
@@ -101,15 +98,13 @@ export default class BaseApplication {
EncryptionService.instance_ = null;
DecryptionWorker.instance_ = null;
this.logger_.info('Base application terminated...');
this.logger_ = null;
this.dbLogger_ = null;
appLogger.info('Base application terminated...');
this.eventEmitter_ = null;
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = null;
}
logger() {
return this.logger_;
return appLogger;
}
public store() {
@@ -289,7 +284,7 @@ export default class BaseApplication {
parentType = BaseModel.TYPE_SMART_FILTER;
}
this.logger().debug('Refreshing notes:', parentType, parentId);
appLogger.debug('Refreshing notes:', parentType, parentId);
const options = {
order: stateUtils.notesOrder(state.settings),
@@ -460,7 +455,7 @@ export default class BaseApplication {
}
async generalMiddleware(store: any, next: any, action: any) {
// this.logger().debug('Reducer action', this.reducerActionToString(action));
// appLogger.debug('Reducer action', this.reducerActionToString(action));
const result = next(action);
const newState = store.getState();
@@ -547,7 +542,11 @@ export default class BaseApplication {
}
if (action.type === 'NOTE_UPDATE_ONE') {
if (!action.changedFields.length || action.changedFields.includes('parent_id') || action.changedFields.includes('encryption_applied')) {
if (!action.changedFields.length ||
action.changedFields.includes('parent_id') ||
action.changedFields.includes('encryption_applied') ||
action.changedFields.includes('is_conflict')
) {
refreshFolders = true;
}
}
@@ -701,35 +700,30 @@ export default class BaseApplication {
const extraFlags = await this.readFlagsFromFile(`${profileDir}/flags.txt`);
initArgs = Object.assign(initArgs, extraFlags);
this.logger_.addTarget(TargetType.File, { path: `${profileDir}/log.txt` });
if (Setting.value('env') === 'dev' && !shim.isTestingEnv()) {
// this.logger_.addTarget(TargetType.Console, { level: Logger.LEVEL_DEBUG });
}
this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_);
const globalLogger = new Logger();
globalLogger.addTarget(TargetType.File, { path: `${profileDir}/log.txt` });
if (Setting.value('appType') === 'desktop') {
globalLogger.addTarget(TargetType.Console);
}
globalLogger.setLevel(initArgs.logLevel);
Logger.initializeGlobalLogger(globalLogger);
reg.setLogger(Logger.create(''));
reg.dispatch = () => {};
BaseService.logger_ = this.logger_;
// require('lib/ntpDate').setLogger(reg.logger());
BaseService.logger_ = globalLogger;
this.dbLogger_.addTarget(TargetType.File, { path: `${profileDir}/log-database.txt` });
this.dbLogger_.setLevel(initArgs.logLevel);
if (Setting.value('appType') === 'desktop') {
this.logger_.addTarget(TargetType.Console, { level: Logger.LEVEL_WARN });
this.dbLogger_.addTarget(TargetType.Console, { level: Logger.LEVEL_WARN });
}
if (Setting.value('env') === 'dev') {
this.dbLogger_.setLevel(Logger.LEVEL_INFO);
}
this.logger_.info(`Profile directory: ${profileDir}`);
appLogger.info(`Profile directory: ${profileDir}`);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
this.database_.setLogExcludedQueryTypes(['SELECT']);
this.database_.setLogger(this.dbLogger_);
this.database_.setLogger(globalLogger);
// if (Setting.value('env') === 'dev') {
// if (shim.isElectron()) {
@@ -756,7 +750,7 @@ export default class BaseApplication {
await loadKeychainServiceAndSettings(KeychainServiceDriver);
this.logger_.info(`Client ID: ${Setting.value('clientId')}`);
appLogger.info(`Client ID: ${Setting.value('clientId')}`);
if (Setting.value('firstStart')) {
const locale = shim.detectAndSetLocale(Setting);
@@ -817,9 +811,9 @@ export default class BaseApplication {
KvStore.instance().setDb(reg.db());
EncryptionService.instance().setLogger(this.logger_);
EncryptionService.instance().setLogger(globalLogger);
BaseItem.encryptionService_ = EncryptionService.instance();
DecryptionWorker.instance().setLogger(this.logger_);
DecryptionWorker.instance().setLogger(globalLogger);
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
DecryptionWorker.instance().setKvStore(KvStore.instance());
await EncryptionService.instance().loadMasterKeysFromSettings();
@@ -828,7 +822,7 @@ export default class BaseApplication {
ResourceFetcher.instance().setFileApi(() => {
return reg.syncTarget().fileApi();
});
ResourceFetcher.instance().setLogger(this.logger_);
ResourceFetcher.instance().setLogger(globalLogger);
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
ResourceFetcher.instance().start();

View File

@@ -26,6 +26,13 @@ interface Target {
source?: string;
}
interface LoggerWrapper {
debug: Function;
info: Function;
warn: Function;
error: Function;
}
class Logger {
// For backward compatibility
@@ -36,6 +43,7 @@ class Logger {
public static LEVEL_DEBUG = LogLevel.Debug;
public static fsDriver_: any = null;
private static globalLogger_: Logger = null;
private targets_: Target[] = [];
private level_: LogLevel = LogLevel.Info;
@@ -46,6 +54,24 @@ class Logger {
return Logger.fsDriver_;
}
public static initializeGlobalLogger(logger: Logger) {
this.globalLogger_ = logger;
}
private static get globalLogger(): Logger {
if (!this.globalLogger_) throw new Error('Global logger has not been initialized!!');
return this.globalLogger_;
}
static create(prefix: string): LoggerWrapper {
return {
debug: (...object: any[]) => this.globalLogger.log(LogLevel.Debug, prefix, ...object),
info: (...object: any[]) => this.globalLogger.log(LogLevel.Info, prefix, ...object),
warn: (...object: any[]) => this.globalLogger.log(LogLevel.Warn, prefix, ...object),
error: (...object: any[]) => this.globalLogger.log(LogLevel.Error, prefix, ...object),
};
}
setLevel(level: LogLevel) {
this.level_ = level;
}
@@ -132,14 +158,12 @@ class Logger {
return this.level();
}
log(level: LogLevel, ...object: any[]) {
public log(level: LogLevel, prefix: string, ...object: any[]) {
if (!this.targets_.length) return;
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
const line = `${timestamp}: `;
for (let i = 0; i < this.targets_.length; i++) {
const target = this.targets_[i];
const targetPrefix = prefix ? prefix : target.prefix;
if (this.targetLevel(target) < level) continue;
@@ -149,26 +173,30 @@ class Logger {
if (level == LogLevel.Warn) fn = 'warn';
if (level == LogLevel.Info) fn = 'info';
const consoleObj = target.console ? target.console : console;
let items = [moment().format('HH:mm:ss')];
if (target.prefix) {
items.push(target.prefix);
}
items = items.concat(...object);
const prefixItems = [moment().format('HH:mm:ss')];
if (targetPrefix) prefixItems.push(targetPrefix);
const items = [`${prefixItems.join(': ')}:`].concat(...object);
consoleObj[fn](...items);
} else if (target.type == 'file') {
const serializedObject = this.objectsToString(...object);
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
const line = [timestamp];
if (targetPrefix) line.push(targetPrefix);
line.push(this.objectsToString(...object));
try {
Logger.fsDriver().appendFileSync(target.path, `${line + serializedObject}\n`);
// TODO: Should log async
Logger.fsDriver().appendFileSync(target.path, `${line.join(': ')}\n`);
} catch (error) {
console.error('Cannot write to log file:', error);
}
} else if (target.type == 'database') {
const msg = this.objectsToString(...object);
const msg = [];
if (targetPrefix) msg.push(targetPrefix);
msg.push(this.objectsToString(...object));
const queries = [
{
sql: 'INSERT INTO logs (`source`, `level`, `message`, `timestamp`) VALUES (?, ?, ?, ?)',
params: [target.source, level, msg, time.unixMs()],
params: [target.source, level, msg.join(': '), time.unixMs()],
},
];
@@ -188,16 +216,16 @@ class Logger {
}
error(...object: any[]) {
return this.log(LogLevel.Error, ...object);
return this.log(LogLevel.Error, null, ...object);
}
warn(...object: any[]) {
return this.log(LogLevel.Warn, ...object);
return this.log(LogLevel.Warn, null, ...object);
}
info(...object: any[]) {
return this.log(LogLevel.Info, ...object);
return this.log(LogLevel.Info, null, ...object);
}
debug(...object: any[]) {
return this.log(LogLevel.Debug, ...object);
return this.log(LogLevel.Debug, null, ...object);
}
static levelStringToId(s: string) {

View File

@@ -92,6 +92,15 @@ shared.updateSettingValue = function(comp, key, value) {
});
};
shared.scheduleSaveSettings = function(comp) {
if (shared.scheduleSaveSettingsIID) clearTimeout(shared.scheduleSaveSettingsIID);
shared.scheduleSaveSettingsIID = setTimeout(() => {
shared.scheduleSaveSettingsIID = null;
shared.saveSettings(comp);
}, 100);
};
shared.saveSettings = function(comp) {
for (const key in comp.state.settings) {
if (!comp.state.settings.hasOwnProperty(key)) continue;

View File

@@ -6,6 +6,7 @@ const shim = require('./shim').default;
class Database {
constructor(driver) {
this.debugMode_ = false;
this.sqlQueryLogEnabled_ = false;
this.driver_ = driver;
this.logger_ = new Logger();
this.logExcludedQueryTypes_ = [];
@@ -238,6 +239,8 @@ class Database {
}
logQuery(sql, params = null) {
if (!this.sqlQueryLogEnabled_) return;
if (this.logExcludedQueryTypes_.length) {
const temp = sql.toLowerCase();
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {

View File

@@ -276,7 +276,7 @@ class Note extends BaseItem {
includeTimestamps: true,
}, options);
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language'];
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language', 'is_conflict'];
if (options.includeTimestamps) {
output.push('updated_time');

View File

@@ -33,7 +33,7 @@ export interface SettingItem {
isEnum?: boolean;
section?: string;
label?(): string;
description?(appType: string): string;
description?: Function;
options?(): any;
appTypes?: string[];
show?(settings: any): boolean;
@@ -44,7 +44,9 @@ export interface SettingItem {
maximum?: number;
step?: number;
onClick?(): void;
unitLabel?(value: any): string;
unitLabel?: Function;
needRestart?: boolean;
autoSave?: boolean;
}
interface SettingItems {
@@ -554,6 +556,17 @@ class Setting extends BaseModel {
},
},
'plugins.states': {
value: '',
type: SettingItemType.Object,
section: 'plugins',
public: true,
appTypes: ['desktop'],
label: () => _('Plugins'),
needRestart: true,
autoSave: true,
},
'plugins.devPluginPaths': {
value: '',
type: SettingItemType.String,
@@ -702,7 +715,7 @@ class Setting extends BaseModel {
description: () => 'CSS file support is provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.',
},
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, section: 'application', public: platform !== 'linux', appTypes: ['desktop'], label: () => _('Automatically update the application') },
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, public: false },
'sync.interval': {
@@ -762,6 +775,16 @@ class Setting extends BaseModel {
},
},
'editor.spellcheckBeta': {
value: false,
type: SettingItemType.Bool,
public: true,
appTypes: ['desktop'],
advanced: true,
label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)',
description: () => 'Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)',
},
'net.customCertificates': {
value: '',
type: SettingItemType.String,
@@ -816,7 +839,7 @@ class Setting extends BaseModel {
minimum: 1,
maximum: 365 * 2,
step: 1,
unitLabel: (value = null) => {
unitLabel: (value: number = null) => {
return value === null ? _('days') : _('%d days', value);
},
label: () => _('Keep note history for'),

View File

@@ -3,7 +3,7 @@ import { Pagination, PaginationOrder } from './types';
export interface ModelFeedPage {
items: any[];
has_more?: boolean;
has_more: boolean;
total?: number;
}
@@ -52,9 +52,8 @@ export default async function(db: any, tableName: string, pagination: Pagination
const rows = await db.selectAll(sql, sqlParams);
const output: ModelFeedPage = { items: rows };
if (rows.length >= pagination.limit) output.has_more = true;
return output;
return {
items: rows,
has_more: rows.length >= pagination.limit,
};
}

View File

@@ -98,35 +98,6 @@
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"argparse": {
@@ -270,6 +241,15 @@
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
"optional": true,
"requires": {
"inherits": "~2.0.0"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -531,6 +511,14 @@
"whatwg-url": "^7.0.0"
}
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@@ -753,6 +741,18 @@
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"optional": true
},
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"optional": true,
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
}
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@@ -1512,6 +1512,11 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multiparty": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
@@ -1527,11 +1532,6 @@
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.1.tgz",
"integrity": "sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA=="
},
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
},
"nanoid": {
"version": "3.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz",
@@ -1545,21 +1545,6 @@
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"no-case": {
@@ -1570,6 +1555,11 @@
"lower-case": "^1.1.1"
}
},
"node-addon-api": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
},
"node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@@ -1587,6 +1577,39 @@
"is-stream": "^1.0.1"
}
},
"node-gyp": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
"integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
"optional": true,
"requires": {
"fstream": "^1.0.0",
"glob": "^7.0.3",
"graceful-fs": "^4.1.2",
"mkdirp": "^0.5.0",
"nopt": "2 || 3",
"npmlog": "0 || 1 || 2 || 3 || 4",
"osenv": "0",
"request": "^2.87.0",
"rimraf": "2",
"semver": "~5.3.0",
"tar": "^2.0.0",
"which": "1"
},
"dependencies": {
"tar": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"optional": true,
"requires": {
"block-stream": "*",
"fstream": "^1.0.12",
"inherits": "2"
}
}
}
},
"node-notifier": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz",
@@ -1645,15 +1668,26 @@
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
},
"dependencies": {
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
}
}
},
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"optional": true,
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
"abbrev": "1"
}
},
"normalize-path": {
@@ -1947,6 +1981,27 @@
"safe-buffer": "^5.1.1"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
@@ -2098,9 +2153,9 @@
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
},
"server-destroy": {
"version": "1.0.1",
@@ -2168,11 +2223,12 @@
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
},
"sqlite3": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz",
"integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.0.tgz",
"integrity": "sha512-rjvqHFUaSGnzxDy2AHCwhHy6Zp6MNJzCPGYju4kD8yi6bze4d1/zMTg6C7JI49b7/EM7jKMTvyfN/4ylBKdwfw==",
"requires": {
"nan": "^2.12.1",
"node-addon-api": "2.0.0",
"node-gyp": "3.x",
"node-pre-gyp": "^0.11.0"
}
},
@@ -2275,6 +2331,21 @@
}
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
"stringify-parameters": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stringify-parameters/-/stringify-parameters-0.0.4.tgz",
@@ -2644,6 +2715,15 @@
"webidl-conversions": "^4.0.2"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"optional": true,
"requires": {
"isexe": "^2.0.0"
}
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",

View File

@@ -64,7 +64,7 @@
"reselect": "^4.0.0",
"server-destroy": "^1.0.1",
"sprintf-js": "^1.1.2",
"sqlite3": "^4.1.1",
"sqlite3": "^5.0.0",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"tar": "^4.4.10",

View File

@@ -787,8 +787,14 @@ const reducer = produce((draft: Draft<State> = defaultState, action: any) => {
for (let i = 0; i < newNotes.length; i++) {
const n = newNotes[i];
if (n.id == modNote.id) {
// Note is still in the same folder
if (isViewingAllNotes || noteIsInFolder(modNote, n.parent_id)) {
if (n.is_conflict && !modNote.is_conflict) {
// Note was a conflict but was moved outside of
// the conflict folder
newNotes.splice(i, 1);
noteFolderHasChanged = true;
movedNotePreviousIndex = i;
} else if (isViewingAllNotes || noteIsInFolder(modNote, n.parent_id)) {
// Note is still in the same folder
// Merge the properties that have changed (in modNote) into
// the object we already have.
newNotes[i] = Object.assign({}, newNotes[i]);

View File

@@ -6,6 +6,8 @@ import { ContentScriptType } from './api/types';
import Logger from '../../Logger';
const EventEmitter = require('events');
const logger = Logger.create('Plugin');
interface ViewControllers {
[key: string]: ViewController;
}
@@ -21,37 +23,33 @@ interface ContentScripts {
export default class Plugin {
private id_: string;
private baseDir_: string;
private manifest_: PluginManifest;
private scriptText_: string;
private enabled_: boolean = true;
private logger_: Logger = null;
private viewControllers_: ViewControllers = {};
private contentScripts_: ContentScripts = {};
private dispatch_: Function;
private eventEmitter_: any;
private devMode_: boolean = false;
constructor(id: string, baseDir: string, manifest: PluginManifest, scriptText: string, logger: Logger, dispatch: Function) {
this.id_ = id;
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function) {
this.baseDir_ = shim.fsDriver().resolve(baseDir);
this.manifest_ = manifest;
this.scriptText_ = scriptText;
this.logger_ = logger;
this.dispatch_ = dispatch;
this.eventEmitter_ = new EventEmitter();
}
public get id(): string {
return this.id_;
return this.manifest.id;
}
public get enabled(): boolean {
return this.enabled_;
public get devMode(): boolean {
return this.devMode_;
}
public set enabled(v: boolean) {
this.enabled_ = v;
public set devMode(v: boolean) {
this.devMode_ = v;
}
public get manifest(): PluginManifest {
@@ -91,7 +89,7 @@ export default class Plugin {
this.contentScripts_[type].push({ id, path: absolutePath });
this.logger_.debug(`Plugin: ${this.id}: Registered content script: ${type}: ${id}: ${absolutePath}`);
logger.debug(`"${this.id}": Registered content script: ${type}: ${id}: ${absolutePath}`);
this.dispatch_({
type: 'PLUGIN_CONTENT_SCRIPTS_ADD',
@@ -119,7 +117,7 @@ export default class Plugin {
}
public deprecationNotice(goneInVersion: string, message: string) {
this.logger_.warn(`Plugin: ${this.id}: DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`);
logger.warn(`"${this.id}": DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`);
}
}

View File

@@ -4,17 +4,45 @@ import Global from './api/Global';
import BasePluginRunner from './BasePluginRunner';
import BaseService from '../BaseService';
import shim from '../../shim';
import { rtrimSlashes } from '../../path-utils';
import { filename, dirname, rtrimSlashes } from '../../path-utils';
import Setting from '../../models/Setting';
import Logger from '../../Logger';
const compareVersions = require('compare-versions');
const { filename, dirname } = require('../../path-utils');
const uslug = require('uslug');
const md5File = require('md5-file/promise');
interface Plugins {
const logger = Logger.create('PluginService');
// Plugin data is split into two:
//
// - First there's the service `plugins` property, which contains the
// plugin static data, as loaded from the plugin file or directory. For
// example, the plugin ID, the manifest, the script files, etc.
//
// - Secondly, there's the `PluginSettings` data, which is dynamic and is
// used for example to enable or disable a plugin. Its state is saved to
// the user's settings.
export interface Plugins {
[key: string]: Plugin;
}
export interface PluginSetting {
enabled: boolean;
deleted: boolean;
}
export function defaultPluginSetting(): PluginSetting {
return {
enabled: true,
deleted: false,
};
}
export interface PluginSettings {
[pluginId: string]: PluginSetting;
}
function makePluginId(source: string): string {
// https://www.npmjs.com/package/slug#options
return uslug(source).substr(0,32);
@@ -49,15 +77,42 @@ export default class PluginService extends BaseService {
return this.plugins_;
}
private setPluginAt(pluginId: string, plugin: Plugin) {
this.plugins_ = {
...this.plugins_,
[pluginId]: plugin,
};
}
private deletePluginAt(pluginId: string) {
if (!this.plugins_[pluginId]) return;
this.plugins_ = { ...this.plugins_ };
delete this.plugins_[pluginId];
}
public pluginById(id: string): Plugin {
if (!this.plugins_[id]) throw new Error(`Plugin not found: ${id}`);
return this.plugins_[id];
}
// public allPluginIds(): string[] {
// return Object.keys(this.plugins_);
// }
public unserializePluginSettings(settings: any): PluginSettings {
const output = { ...settings };
for (const pluginId in output) {
output[pluginId] = {
...defaultPluginSetting(),
...output[pluginId],
};
}
return output;
}
public serializePluginSettings(settings: PluginSettings): any {
return JSON.stringify(settings);
}
private async parsePluginJsBundle(jsBundleString: string) {
const scriptText = jsBundleString;
@@ -146,7 +201,7 @@ export default class PluginService extends BaseService {
}
}
private async loadPluginFromPath(path: string): Promise<Plugin> {
public async loadPluginFromPath(path: string): Promise<Plugin> {
path = rtrimSlashes(path);
const fsDriver = shim.fsDriver();
@@ -161,7 +216,7 @@ export default class PluginService extends BaseService {
distPath = `${path}/dist`;
}
this.logger().info(`PluginService: Loading plugin from ${path}`);
logger.info(`Loading plugin from ${path}`);
const scriptText = await fsDriver.readFile(`${distPath}/index.js`);
const manifestText = await fsDriver.readFile(`${distPath}/manifest.json`);
@@ -189,79 +244,145 @@ export default class PluginService extends BaseService {
}
const manifest = manifestFromObject(manifestObj);
const pluginId = manifest.id;
// After transforming the plugin path to an ID, multiple plugins might end up with the same ID. For
// example "MyPlugin" and "myplugin" would have the same ID. Technically it's possible to have two
// such folders but to keep things sane we disallow it.
if (this.plugins_[pluginId]) throw new Error(`There is already a plugin with this ID: ${pluginId}`);
const plugin = new Plugin(pluginId, baseDir, manifest, scriptText, this.logger(), (action: any) => this.store_.dispatch(action));
if (compareVersions(this.appVersion_, manifest.app_min_version) < 0) {
this.logger().info(`PluginService: Plugin "${pluginId}" was disabled because it requires a newer version of Joplin.`, manifest);
plugin.enabled = false;
} else {
this.store_.dispatch({
type: 'PLUGIN_ADD',
plugin: {
id: pluginId,
views: {},
contentScripts: {},
},
});
}
const plugin = new Plugin(baseDir, manifest, scriptText, (action: any) => this.store_.dispatch(action));
for (const msg of deprecationNotices) {
plugin.deprecationNotice('1.5', msg);
}
// Sanity check, although at that point the plugin ID should have
// been set, either automatically, or because it was defined in the
// manifest.
if (!plugin.id) throw new Error('Could not load plugin: ID is not set');
return plugin;
}
public async loadAndRunPlugins(pluginDirOrPaths: string | string[]) {
private pluginEnabled(settings: PluginSettings, pluginId: string): boolean {
if (!settings[pluginId]) return true;
return settings[pluginId].enabled !== false;
}
public async loadAndRunPlugins(pluginDirOrPaths: string | string[], settings: PluginSettings, devMode: boolean = false) {
let pluginPaths = [];
if (Array.isArray(pluginDirOrPaths)) {
pluginPaths = pluginDirOrPaths;
} else {
pluginPaths = (await shim.fsDriver().readDirStats(pluginDirOrPaths))
.filter((stat: any) => (stat.isDirectory() || stat.path.toLowerCase().endsWith('.js')))
.filter((stat: any) => {
if (stat.isDirectory()) return true;
if (stat.path.toLowerCase().endsWith('.js')) return true;
if (stat.path.toLowerCase().endsWith('.jpl')) return true;
return false;
})
.map((stat: any) => `${pluginDirOrPaths}/${stat.path}`);
}
for (const pluginPath of pluginPaths) {
if (pluginPath.indexOf('_') === 0) {
this.logger().info(`PluginService: Plugin name starts with "_" and has not been loaded: ${pluginPath}`);
logger.info(`Plugin name starts with "_" and has not been loaded: ${pluginPath}`);
continue;
}
try {
const plugin = await this.loadPluginFromPath(pluginPath);
// After transforming the plugin path to an ID, multiple plugins might end up with the same ID. For
// example "MyPlugin" and "myplugin" would have the same ID. Technically it's possible to have two
// such folders but to keep things sane we disallow it.
if (this.plugins_[plugin.id]) throw new Error(`There is already a plugin with this ID: ${plugin.id}`);
this.setPluginAt(plugin.id, plugin);
if (!this.pluginEnabled(settings, plugin.id)) {
logger.info(`Not running disabled plugin: "${plugin.id}"`);
continue;
}
plugin.devMode = devMode;
await this.runPlugin(plugin);
} catch (error) {
this.logger().error(`PluginService: Could not load plugin: ${pluginPath}`, error);
logger.error(`Could not load plugin: ${pluginPath}`, error);
}
}
}
public async runPlugin(plugin: Plugin) {
this.plugins_[plugin.id] = plugin;
const pluginApi = new Global(this.logger(), this.platformImplementation_, plugin, this.store_);
if (compareVersions(this.appVersion_, plugin.manifest.app_min_version) < 0) {
throw new Error(`Plugin "${plugin.id}" was disabled because it requires Joplin version ${plugin.manifest.app_min_version} and current version is ${this.appVersion_}.`);
} else {
this.store_.dispatch({
type: 'PLUGIN_ADD',
plugin: {
id: plugin.id,
views: {},
contentScripts: {},
},
});
}
const pluginApi = new Global(this.platformImplementation_, plugin, this.store_);
return this.runner_.run(plugin, pluginApi);
}
// public async handleDisabledPlugins() {
// const enabledPlugins = this.allPluginIds();
// const v = await this.kvStore_.value<string>('pluginService.lastEnabledPlugins');
// const lastEnabledPlugins = v ? JSON.parse(v) : [];
public async installPlugin(jplPath: string): Promise<Plugin> {
logger.info(`Installing plugin: "${jplPath}"`);
// const disabledPlugins = [];
// for (const id of lastEnabledPlugins) {
// if (!enabledPlugins.includes(id)) disabledPlugins.push(id);
// }
// Before moving the plugin to the profile directory, we load it
// from where it is now to check that it is valid and to retrieve
// the plugin ID.
const preloadedPlugin = await this.loadPluginFromPath(jplPath);
// await this.kvStore_.setValue('pluginService.lastEnabledPlugins', JSON.stringify(enabledPlugins));
// }
const destPath = `${Setting.value('pluginDir')}/${preloadedPlugin.id}.jpl`;
await shim.fsDriver().copy(jplPath, destPath);
// Now load it from the profile directory
const plugin = await this.loadPluginFromPath(destPath);
if (!this.plugins_[plugin.id]) this.setPluginAt(plugin.id, plugin);
return plugin;
}
private async pluginPath(pluginId: string) {
const stats = await shim.fsDriver().readDirStats(Setting.value('pluginDir'), { recursive: false });
for (const stat of stats) {
if (filename(stat.path) === pluginId) {
return `${Setting.value('pluginDir')}/${stat.path}`;
}
}
return null;
}
public async uninstallPlugin(pluginId: string) {
logger.info(`Uninstalling plugin: "${pluginId}"`);
const path = await this.pluginPath(pluginId);
if (!path) {
// Plugin might have already been deleted
logger.error(`Could not find plugin path to uninstall - nothing will be done: ${pluginId}`);
} else {
await shim.fsDriver().remove(path);
}
this.deletePluginAt(pluginId);
}
public async uninstallPlugins(settings: PluginSettings): Promise<PluginSettings> {
let newSettings = settings;
for (const pluginId in settings) {
if (settings[pluginId].deleted) {
await this.uninstallPlugin(pluginId);
newSettings = { ...settings };
delete newSettings[pluginId];
}
}
return newSettings;
}
}

View File

@@ -1,6 +1,5 @@
import Plugin from '../Plugin';
import Joplin from './Joplin';
import Logger from '../../../Logger';
/**
* @ignore
@@ -16,8 +15,8 @@ export default class Global {
private requireWhiteList_: string[] = null;
// private consoleWrapper_:any = null;
constructor(logger: Logger, implementation: any, plugin: Plugin, store: any) {
this.joplin_ = new Joplin(logger, implementation.joplin, plugin, store);
constructor(implementation: any, plugin: Plugin, store: any) {
this.joplin_ = new Joplin(implementation.joplin, plugin, store);
// this.consoleWrapper_ = this.createConsoleWrapper(plugin.id);
}

View File

@@ -7,7 +7,6 @@ import JoplinCommands from './JoplinCommands';
import JoplinViews from './JoplinViews';
import JoplinInterop from './JoplinInterop';
import JoplinSettings from './JoplinSettings';
import Logger from '../../../Logger';
/**
* This is the main entry point to the Joplin API. You can access various services using the provided accessors.
@@ -35,9 +34,9 @@ export default class Joplin {
private interop_: JoplinInterop = null;
private settings_: JoplinSettings = null;
constructor(logger: Logger, implementation: any, plugin: Plugin, store: any) {
constructor(implementation: any, plugin: Plugin, store: any) {
this.data_ = new JoplinData();
this.plugins_ = new JoplinPlugins(logger, plugin);
this.plugins_ = new JoplinPlugins(plugin);
this.workspace_ = new JoplinWorkspace(implementation.workspace, store);
this.filters_ = new JoplinFilters();
this.commands_ = new JoplinCommands();

View File

@@ -2,16 +2,16 @@ import Plugin from '../Plugin';
import Logger from '../../../Logger';
import { ContentScriptType, Script } from './types';
const logger = Logger.create('joplin.plugins');
/**
* This class provides access to plugin-related features.
*/
export default class JoplinPlugins {
private logger: Logger;
private plugin: Plugin;
public constructor(logger: Logger, plugin: Plugin) {
this.logger = logger;
public constructor(plugin: Plugin) {
this.plugin = plugin;
}
@@ -31,7 +31,7 @@ export default class JoplinPlugins {
if (script.onStart) {
const startTime = Date.now();
this.logger.info(`Starting plugin: ${this.plugin.id}`);
logger.info(`Starting plugin: ${this.plugin.id}`);
// We don't use `await` when calling onStart because the plugin might be awaiting
// in that call too (for example, when opening a dialog on startup) so we don't
@@ -42,9 +42,9 @@ export default class JoplinPlugins {
// be handled correctly by loggers, etc.
const newError: Error = new Error(error.message);
newError.stack = error.stack;
this.logger.error(`Uncaught exception in plugin "${this.plugin.id}":`, newError);
logger.error(`Uncaught exception in plugin "${this.plugin.id}":`, newError);
}).then(() => {
this.logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
this.plugin.emit('started');
});
}

View File

@@ -51,7 +51,7 @@ const gunzipFile = function(source, destination) {
});
};
function shimInit(sharp = null, keytar = null) {
function shimInit(sharp = null, keytar = null, React = null) {
keytar = (shim.isWindows() || shim.isMac()) && !shim.isPortable() ? keytar : null;
shim.fsDriver = () => {
@@ -67,6 +67,12 @@ function shimInit(sharp = null, keytar = null) {
return shim.fsDriver_;
};
if (React) {
shim.react = () => {
return React;
};
}
shim.randomBytes = async count => {
const buffer = require('crypto').randomBytes(count);
return Array.from(buffer);

View File

@@ -1,6 +1,21 @@
import { ResourceEntity } from './services/database/types';
let isTestingEnv_ = false;
// We need to ensure that there's only one instance of React being used by
// all the packages. In particular, the lib might need React to define
// generic hooks, but it shouldn't have React in its dependencies as that
// would cause the following error:
//
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
//
// So instead, the **applications** include React as a dependency, then
// pass it to any other packages using the shim. Essentially, only one
// package should require React, and in our case that should be one of the
// applications (app-desktop, app-mobile, etc.) since we are sure they
// won't be dependency to other packages (unlike the lib which can be
// included anywhere).
let react_: any = null;
const shim = {

View File

@@ -1,6 +1,6 @@
const htmlUtils = require('./htmlUtils');
const utils = require('./utils');
// const noteStyle = require('./noteStyle').default;
import htmlUtils from './htmlUtils';
import linkReplacement from './MdToHtml/linkReplacement';
import utils from './utils';
// TODO: fix
// const Setting = require('@joplin/lib/models/Setting').default;
@@ -13,9 +13,45 @@ const md5 = require('md5');
// relatively small.
const inMemoryCache = new InMemoryCache(10);
class HtmlToHtml {
constructor(options) {
if (!options) options = {};
interface FsDriver {
writeFile: Function;
exists: Function;
cacheCssToFile: Function;
}
interface Options {
ResourceModel: any;
resourceBaseUrl?: string;
fsDriver?: FsDriver;
}
interface RenderOptions {
splitted: boolean;
bodyOnly: boolean;
externalAssetsOnly: boolean;
resources: any;
postMessageSyntax: string;
enableLongPress: boolean;
}
interface RenderResult {
html: string;
pluginAssets: any[];
}
export default class HtmlToHtml {
private resourceBaseUrl_;
private ResourceModel_;
private cache_;
private fsDriver_:any;
constructor(options:Options = null) {
options = {
ResourceModel: null,
...options
};
this.resourceBaseUrl_ = 'resourceBaseUrl' in options ? options.resourceBaseUrl : null;
this.ResourceModel_ = options.ResourceModel;
this.cache_ = inMemoryCache;
@@ -36,7 +72,7 @@ class HtmlToHtml {
return this.fsDriver_;
}
splitHtml(html) {
splitHtml(html:string) {
const trimmedHtml = html.trimStart();
if (trimmedHtml.indexOf('<style>') !== 0) return { html: html, css: '' };
@@ -49,17 +85,20 @@ class HtmlToHtml {
};
}
async allAssets(/* theme*/) {
async allAssets(/* theme*/):Promise<any[]> {
return []; // TODO
}
// Note: the "theme" variable is ignored and instead the light theme is
// always used for HTML notes.
// See: https://github.com/laurent22/joplin/issues/3698
async render(markup, _theme, options) {
options = Object.assign({}, {
async render(markup:string, _theme:any, options:RenderOptions):Promise<RenderResult> {
options = {
splitted: false,
}, options);
postMessageSyntax: 'postMessage',
enableLongPress: false,
...options,
};
const cacheKey = md5(escape(markup));
let html = this.cache_.value(cacheKey);
@@ -67,7 +106,7 @@ class HtmlToHtml {
if (!html) {
html = htmlUtils.sanitizeHtml(markup);
html = htmlUtils.processImageTags(html, data => {
html = htmlUtils.processImageTags(html, (data:any) => {
if (!data.src) return null;
const r = utils.imageReplacement(this.ResourceModel_, data.src, options.resources, this.resourceBaseUrl_);
@@ -85,6 +124,24 @@ class HtmlToHtml {
};
}
});
html = htmlUtils.processAnchorTags(html, (data:any) => {
if (!data.href) return null;
const r = linkReplacement(data.href, {
resources: options.resources,
ResourceModel: this.ResourceModel_,
postMessageSyntax: options.postMessageSyntax,
enableLongPress: options.enableLongPress,
});
if (!r) return null;
return {
type: 'replaceElement',
html: r,
};
});
}
this.cache_.setValue(cacheKey, html, 1000 * 60 * 10);
@@ -98,13 +155,13 @@ class HtmlToHtml {
// const lightTheme = themeStyle(Setting.THEME_LIGHT);
// let cssStrings = noteStyle(lightTheme);
let cssStrings = [];
let cssStrings:string[] = [];
if (options.splitted) {
const splitted = this.splitHtml(html);
cssStrings = [splitted.css].concat(cssStrings);
const output = {
const output:RenderResult = {
html: splitted.html,
pluginAssets: [],
};
@@ -124,5 +181,3 @@ class HtmlToHtml {
};
}
}
module.exports = HtmlToHtml;

View File

@@ -1,6 +1,6 @@
import MdToHtml from './MdToHtml';
const HtmlToHtml = require('./HtmlToHtml');
const htmlUtils = require('./htmlUtils');
import HtmlToHtml from './HtmlToHtml';
import htmlUtils from './htmlUtils';
const MarkdownIt = require('markdown-it');
export enum MarkupLanguage {

View File

@@ -68,9 +68,9 @@ function slugify(s: string): string {
const inMemoryCache = new InMemoryCache(20);
export interface ExtraRendererRule {
id: string,
module: any,
assetPath: string,
id: string;
module: any;
assetPath: string;
}
export interface Options {
@@ -138,10 +138,6 @@ export interface RuleOptions {
// to display a context menu. Used in `image.ts` and `link_open.ts`
enableLongPress?: boolean;
// Used in mobile app when enableLongPress = true. Tells for how long
// the resource should be pressed before the menu is shown.
longPressDelay?: number;
// Use by `link_open` rule.
// linkRenderingType = 1 is the regular rendering and clicking on it is handled via embedded JS (in onclick attribute)
// linkRenderingType = 2 gives a plain link with no JS. Caller needs to handle clicking on the link.

View File

@@ -0,0 +1,53 @@
import linkReplacement from './linkReplacement';
describe('linkReplacement', () => {
test('should handle non-resource links', () => {
const r = linkReplacement('https://example.com/test');
expect(r).toBe(`<a data-from-md href='https://example.com/test' onclick='postMessage("https://example.com/test", { resourceId: "" }); return false;'>`);
});
test('should handle non-resource links - simple rendering', () => {
const r = linkReplacement('https://example.com/test', { linkRenderingType: 2 });
expect(r).toBe(`<a data-from-md href='https://example.com/test'>`);
});
test('should handle resource links - downloaded status', () => {
const resourceId = 'f6afba55bdf74568ac94f8d1e3578d2c';
const r = linkReplacement(':/' + resourceId, {
ResourceModel: {},
resources: {
[resourceId]: {
item: {},
localState: {
fetch_status: 2, // FETCH_STATUS_DONE
},
},
},
});
expect(r).toBe(`<a data-from-md data-resource-id='${resourceId}' href='#' onclick='postMessage("joplin://${resourceId}", { resourceId: "${resourceId}" }); return false;'><span class="resource-icon fa-joplin"></span>`);
});
test('should handle resource links - idle status', () => {
const resourceId = 'f6afba55bdf74568ac94f8d1e3578d2c';
const r = linkReplacement(':/' + resourceId, {
ResourceModel: {},
resources: {
[resourceId]: {
item: {},
localState: {
fetch_status: 0, // FETCH_STATUS_IDLE
},
},
},
});
// Since the icon is embedded as SVG, we only check for the prefix
const expectedPrefix = `<a class="not-loaded-resource resource-status-notDownloaded" data-resource-id="${resourceId}"><img src="data:image/svg+xml;utf8`
expect(r.indexOf(expectedPrefix)).toBe(0);
});
});

View File

@@ -0,0 +1,104 @@
import utils from '../utils';
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode;
const urlUtils = require('../urlUtils.js');
const { getClassNameForMimeType } = require('font-awesome-filetypes');
export interface Options {
title?:string,
resources?:any,
ResourceModel?:any,
linkRenderingType?:number,
plainResourceRendering?:boolean,
postMessageSyntax?:string,
enableLongPress?:boolean,
}
export default function(href:string, options:Options = null) {
options = {
title: '',
resources: {},
ResourceModel: null,
linkRenderingType: 1,
plainResourceRendering: false,
postMessageSyntax: 'postMessage',
enableLongPress: false,
...options,
};
const resourceHrefInfo = urlUtils.parseResourceUrl(href);
const isResourceUrl = options.resources && !!resourceHrefInfo;
let title = options.title;
let resourceIdAttr = '';
let icon = '';
let hrefAttr = '#';
let mime = '';
let resourceId = '';
if (isResourceUrl) {
resourceId = resourceHrefInfo.itemId;
const result = options.resources[resourceId];
const resourceStatus = utils.resourceStatus(options.ResourceModel, result);
if (result && result.item) {
if (!title) title = result.item.title;
mime = result.item.mime;
}
if (result && resourceStatus !== 'ready' && !options.plainResourceRendering) {
const icon = utils.resourceStatusFile(resourceStatus);
return `<a class="not-loaded-resource resource-status-${resourceStatus}" data-resource-id="${resourceId}">` + `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>`;
} else {
href = `joplin://${resourceId}`;
if (resourceHrefInfo.hash) href += `#${resourceHrefInfo.hash}`;
resourceIdAttr = `data-resource-id='${resourceId}'`;
let iconType = mime ? getClassNameForMimeType(mime) : 'fa-joplin';
// Icons are defined in lib/renderers/noteStyle using inline svg
// The icons are taken from fork-awesome but use the font-awesome naming scheme in order
// to be more compatible with the getClass library
icon = `<span class="resource-icon ${iconType}"></span>`;
}
} else {
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
// link. This allows the link to be exported too when exporting to PDF.
hrefAttr = href;
}
// A single quote is valid in a URL but we don't want any because the
// href is already enclosed in single quotes.
// https://github.com/laurent22/joplin/issues/2030
href = href.replace(/'/g, '%27');
let js = `${options.postMessageSyntax}(${JSON.stringify(href)}, { resourceId: ${JSON.stringify(resourceId)} }); return false;`;
if (options.enableLongPress && !!resourceId) {
const onClick = `${options.postMessageSyntax}(${JSON.stringify(href)})`;
const onLongClick = `${options.postMessageSyntax}("longclick:${resourceId}")`;
const touchStart = `t=setTimeout(()=>{t=null; ${onLongClick};}, ${utils.longPressDelay});`;
const cancel = 'if (!!t) {clearTimeout(t); t=null;';
const touchEnd = `${cancel} ${onClick};}`;
js = `ontouchstart='${touchStart}' ontouchend='${touchEnd}' ontouchcancel='${cancel} ontouchmove="${cancel}'`;
} else {
js = `onclick='${js}'`;
}
if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place
const attrHtml = [];
attrHtml.push('data-from-md');
if (resourceIdAttr) attrHtml.push(resourceIdAttr);
if (title) attrHtml.push(`title='${htmlentities(title)}'`);
if (mime) attrHtml.push(`type='${htmlentities(mime)}'`);
if (options.plainResourceRendering || options.linkRenderingType === 2) {
icon = '';
attrHtml.push(`href='${htmlentities(href)}'`);
} else {
attrHtml.push(`href='${hrefAttr}'`);
if (js) attrHtml.push(js);
}
return `<a ${attrHtml.join(' ')}>${icon}`;
}

View File

@@ -1,7 +1,6 @@
import { RuleOptions } from '../../MdToHtml';
const htmlUtils = require('../../htmlUtils.js');
const utils = require('../../utils');
import htmlUtils from '../../htmlUtils';
import utils from '../../utils';
function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) {
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl);

View File

@@ -1,7 +1,6 @@
import { RuleOptions } from '../../MdToHtml';
const utils = require('../../utils');
const htmlUtils = require('../../htmlUtils.js');
import htmlUtils from '../../htmlUtils';
import utils from '../../utils';
function plugin(markdownIt: any, ruleOptions: RuleOptions) {
const defaultRender = markdownIt.renderer.rules.image;
@@ -23,7 +22,7 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) {
const id = r['data-resource-id'];
const longPressHandler = `${ruleOptions.postMessageSyntax}('longclick:${id}')`;
const touchStart = `t=setTimeout(()=>{t=null; ${longPressHandler};}, ${ruleOptions.longPressDelay});`;
const touchStart = `t=setTimeout(()=>{t=null; ${longPressHandler};}, ${utils.longPressDelay});`;
const cancel = 'if (!!t) clearTimeout(t); t=null';
js = ` ontouchstart="${touchStart}" ontouchend="${cancel}" ontouchcancel="${cancel}" ontouchmove="${cancel}"`;

View File

@@ -1,95 +1,26 @@
import { RuleOptions } from '../../MdToHtml';
import linkReplacement from '../linkReplacement';
import utils from '../../utils';
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode;
const utils = require('../../utils');
const urlUtils = require('../../urlUtils.js');
const { getClassNameForMimeType } = require('font-awesome-filetypes');
function plugin(markdownIt: any, ruleOptions: RuleOptions) {
markdownIt.renderer.rules.link_open = function(tokens: any[], idx: number) {
const token = tokens[idx];
let href = utils.getAttr(token.attrs, 'href');
const href = utils.getAttr(token.attrs, 'href');
const resourceHrefInfo = urlUtils.parseResourceUrl(href);
const isResourceUrl = ruleOptions.resources && !!resourceHrefInfo;
let title = utils.getAttr(token.attrs, 'title', isResourceUrl ? '' : href);
const title = utils.getAttr(token.attrs, 'title', isResourceUrl ? '' : href);
let resourceIdAttr = '';
let icon = '';
let hrefAttr = '#';
let mime = '';
let resourceId = '';
if (isResourceUrl) {
resourceId = resourceHrefInfo.itemId;
const result = ruleOptions.resources[resourceId];
const resourceStatus = utils.resourceStatus(ruleOptions.ResourceModel, result);
if (result && result.item) {
title = utils.getAttr(token.attrs, 'title', result.item.title);
mime = result.item.mime;
}
if (result && resourceStatus !== 'ready' && !ruleOptions.plainResourceRendering) {
const icon = utils.resourceStatusFile(resourceStatus);
return `<a class="not-loaded-resource resource-status-${resourceStatus}" data-resource-id="${resourceId}">` + `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>`;
} else {
href = `joplin://${resourceId}`;
if (resourceHrefInfo.hash) href += `#${resourceHrefInfo.hash}`;
resourceIdAttr = `data-resource-id='${resourceId}'`;
let iconType = getClassNameForMimeType(mime);
if (!mime) {
iconType = 'fa-joplin';
}
// Icons are defined in lib/renderers/noteStyle using inline svg
// The icons are taken from fork-awesome but use the font-awesome naming scheme in order
// to be more compatible with the getClass library
icon = `<span class="resource-icon ${iconType}"></span>`;
}
} else {
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
// link. This allows the link to be exported too when exporting to PDF.
hrefAttr = href;
}
// A single quote is valid in a URL but we don't want any because the
// href is already enclosed in single quotes.
// https://github.com/laurent22/joplin/issues/2030
href = href.replace(/'/g, '%27');
let js = `${ruleOptions.postMessageSyntax}(${JSON.stringify(href)}, { resourceId: ${JSON.stringify(resourceId)} }); return false;`;
if (ruleOptions.enableLongPress && !!resourceId) {
const onClick = `${ruleOptions.postMessageSyntax}(${JSON.stringify(href)})`;
const onLongClick = `${ruleOptions.postMessageSyntax}("longclick:${resourceId}")`;
const touchStart = `t=setTimeout(()=>{t=null; ${onLongClick};}, ${ruleOptions.longPressDelay});`;
const cancel = 'if (!!t) {clearTimeout(t); t=null;';
const touchEnd = `${cancel} ${onClick};}`;
js = `ontouchstart='${touchStart}' ontouchend='${touchEnd}' ontouchcancel='${cancel} ontouchmove="${cancel}'`;
} else {
js = `onclick='${js}'`;
}
if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place
const attrHtml = [];
attrHtml.push('data-from-md');
if (resourceIdAttr) attrHtml.push(resourceIdAttr);
if (title) attrHtml.push(`title='${htmlentities(title)}'`);
if (mime) attrHtml.push(`type='${htmlentities(mime)}'`);
if (ruleOptions.plainResourceRendering || ruleOptions.linkRenderingType === 2) {
icon = '';
attrHtml.push(`href='${htmlentities(href)}'`);
// return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${htmlentities(href)}' type='${htmlentities(mime)}'>`;
} else {
attrHtml.push(`href='${hrefAttr}'`);
if (js) attrHtml.push(js);
// return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${hrefAttr}' ${js} type='${htmlentities(mime)}'>${icon}`;
}
return `<a ${attrHtml.join(' ')}>${icon}`;
return linkReplacement(href, {
title,
resources: ruleOptions.resources,
ResourceModel: ruleOptions.ResourceModel,
linkRenderingType: ruleOptions.linkRenderingType,
plainResourceRendering: ruleOptions.plainResourceRendering,
postMessageSyntax: ruleOptions.postMessageSyntax,
enableLongPress: ruleOptions.enableLongPress,
});
};
}

View File

@@ -1,7 +1,7 @@
import { RuleOptions } from '../../MdToHtml';
import htmlUtils from '../../htmlUtils';
const md5 = require('md5');
const htmlUtils = require('../../htmlUtils');
export default {
plugin: function(markdownIt: any, ruleOptions: RuleOptions) {

View File

@@ -6,6 +6,8 @@ const htmlparser2 = require('@joplin/fork-htmlparser2');
// https://stackoverflow.com/a/16119722/561309
const imageRegex = /<img([\s\S]*?)src=["']([\s\S]*?)["']([\s\S]*?)>/gi;
const anchorRegex = /<a([\s\S]*?)href=["']([\s\S]*?)["']([\s\S]*?)>/gi;
const selfClosingElements = [
'area',
'base',
@@ -30,7 +32,7 @@ const selfClosingElements = [
class HtmlUtils {
attributesHtml(attr) {
attributesHtml(attr:any) {
const output = [];
for (const n in attr) {
@@ -41,10 +43,10 @@ class HtmlUtils {
return output.join(' ');
}
processImageTags(html, callback) {
processImageTags(html:string, callback:Function) {
if (!html) return '';
return html.replace(imageRegex, (v, before, src, after) => {
return html.replace(imageRegex, (_v, before, src, after) => {
const action = callback({ src: src });
if (!action) return `<img${before}src="${src}"${after}>`;
@@ -66,15 +68,40 @@ class HtmlUtils {
});
}
isSelfClosingTag(tagName) {
processAnchorTags(html:string, callback:Function) {
if (!html) return '';
return html.replace(anchorRegex, (_v, before, href, after) => {
const action = callback({ href: href });
if (!action) return `<a${before}href="${href}"${after}>`;
if (action.type === 'replaceElement') {
return action.html;
}
if (action.type === 'replaceSource') {
return `<img${before}href="${action.href}"${after}>`;
}
if (action.type === 'setAttributes') {
const attrHtml = this.attributesHtml(action.attrs);
return `<img${before}${attrHtml}${after}>`;
}
throw new Error(`Invalid action: ${action.type}`);
});
}
isSelfClosingTag(tagName:string) {
return selfClosingElements.includes(tagName.toLowerCase());
}
// TODO: copied from @joplin/lib
stripHtml(html) {
const output = [];
stripHtml(html:string) {
const output:string[] = [];
const tagStack = [];
const tagStack:string[] = [];
const currentTag = () => {
if (!tagStack.length) return '';
@@ -85,16 +112,16 @@ class HtmlUtils {
const parser = new htmlparser2.Parser({
onopentag: (name) => {
onopentag: (name:string) => {
tagStack.push(name.toLowerCase());
},
ontext: (decodedText) => {
ontext: (decodedText:string) => {
if (disallowedTags.includes(currentTag())) return;
output.push(decodedText);
},
onclosetag: (name) => {
onclosetag: (name:string) => {
if (currentTag() === name.toLowerCase()) tagStack.pop();
},
@@ -106,16 +133,16 @@ class HtmlUtils {
return output.join('').replace(/\s+/g, ' ');
}
sanitizeHtml(html, options = null) {
sanitizeHtml(html:string, options:any = null) {
options = Object.assign({}, {
// If true, adds a "jop-noMdConv" class to all the tags.
// It can be used afterwards to restore HTML tags in Markdown.
addNoMdConvClass: false,
}, options);
const output = [];
const output:string[] = [];
const tagStack = [];
const tagStack:string[] = [];
const currentTag = () => {
if (!tagStack.length) return '';
@@ -135,7 +162,7 @@ class HtmlUtils {
const parser = new htmlparser2.Parser({
onopentag: (name, attrs) => {
onopentag: (name:string, attrs:any) => {
tagStack.push(name.toLowerCase());
if (disallowedTags.includes(currentTag())) return;
@@ -171,7 +198,7 @@ class HtmlUtils {
output.push(`<${name}${attrHtml}${closingSign}`);
},
ontext: (decodedText) => {
ontext: (decodedText:string) => {
if (disallowedTags.includes(currentTag())) return;
if (currentTag() === 'style') {
@@ -184,7 +211,7 @@ class HtmlUtils {
}
},
onclosetag: (name) => {
onclosetag: (name:string) => {
const current = currentTag();
if (current === name.toLowerCase()) tagStack.pop();
@@ -206,6 +233,4 @@ class HtmlUtils {
}
const htmlUtils = new HtmlUtils();
module.exports = htmlUtils;
export default new HtmlUtils();

View File

@@ -1,9 +1,9 @@
import MarkupToHtml, { MarkupLanguage } from './MarkupToHtml';
import MdToHtml from './MdToHtml';
const HtmlToHtml = require('./HtmlToHtml');
import HtmlToHtml from './HtmlToHtml';
import utils from './utils';
const setupLinkify = require('./MdToHtml/setupLinkify');
const assetsToHeaders = require('./assetsToHeaders');
const utils = require('./utils');
export {
MarkupToHtml,

View File

@@ -0,0 +1,191 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls and instances between every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'v8',
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: 'node',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: [
'**/*.test.js',
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

File diff suppressed because it is too large Load Diff

View File

@@ -12,17 +12,20 @@
"buildAssets": "node Tools/buildAssets.js",
"prepublishOnly": "npm run buildAssets",
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json"
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json",
"test": "jest",
"test-ci": "test"
},
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^14.14.6",
"jasmine": "^3.5.0",
"jest": "^26.6.3",
"typescript": "^4.0.5"
},
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.8",
"@types/jest": "^26.0.15",
"font-awesome-filetypes": "^2.1.0",
"fs-extra": "^8.1.0",
"highlight.js": "^10.2.1",

View File

@@ -9,9 +9,9 @@ const FetchStatuses = {
FETCH_STATUS_ERROR: 3,
};
const utils = {};
const utils:any = {};
utils.getAttr = function(attrs, name, defaultValue = null) {
utils.getAttr = function(attrs:string[], name:string, defaultValue:string = null) {
for (let i = 0; i < attrs.length; i++) {
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
}
@@ -63,12 +63,12 @@ utils.loaderImage = function() {
`;
};
utils.resourceStatusImage = function(status) {
utils.resourceStatusImage = function(status:string) {
if (status === 'notDownloaded') return utils.notDownloadedResource();
return utils.resourceStatusFile(status);
};
utils.resourceStatusFile = function(status) {
utils.resourceStatusFile = function(status:string) {
if (status === 'notDownloaded') return utils.notDownloadedResource();
if (status === 'downloading') return utils.loaderImage();
if (status === 'encrypted') return utils.loaderImage();
@@ -77,7 +77,7 @@ utils.resourceStatusFile = function(status) {
throw new Error(`Unknown status: ${status}`);
};
utils.resourceStatusIndex = function(status) {
utils.resourceStatusIndex = function(status:string) {
if (status === 'error') return -1;
if (status === 'notDownloaded') return 0;
if (status === 'downloading') return 1;
@@ -87,7 +87,7 @@ utils.resourceStatusIndex = function(status) {
throw new Error(`Unknown status: ${status}`);
};
utils.resourceStatusName = function(index) {
utils.resourceStatusName = function(index:number) {
if (index === -1) return 'error';
if (index === 0) return 'notDownloaded';
if (index === 1) return 'downloading';
@@ -97,7 +97,7 @@ utils.resourceStatusName = function(index) {
throw new Error(`Unknown index: ${index}`);
};
utils.resourceStatus = function(ResourceModel, resourceInfo) {
utils.resourceStatus = function(ResourceModel:any, resourceInfo:any) {
if (!ResourceModel) return 'ready';
let resourceStatus = 'ready';
@@ -122,7 +122,7 @@ utils.resourceStatus = function(ResourceModel, resourceInfo) {
return resourceStatus;
};
utils.imageReplacement = function(ResourceModel, src, resources, resourceBaseUrl) {
utils.imageReplacement = function(ResourceModel:any, src:string, resources:any, resourceBaseUrl:string) {
if (!ResourceModel || !resources) return null;
if (!ResourceModel.isResourceUrl(src)) return null;
@@ -151,4 +151,8 @@ utils.imageReplacement = function(ResourceModel, src, resources, resourceBaseUrl
return null;
};
module.exports = utils;
// Used in mobile app when enableLongPress = true. Tells for how long
// the resource should be pressed before the menu is shown.
utils.longPressDelay = 500;
export default utils;

View File

@@ -2,8 +2,10 @@
// Dependencies:
//
// sudo apt install gettext
// sudo apt install translate-toolkit
// sudo apt install gettext sudo apt install translate-toolkit
//
// gettext v21+ is required as versions before that have bugs when parsing
// JavaScript template strings which means we would lose translations.
const rootDir = `${__dirname}/../..`;

View File

@@ -56,6 +56,7 @@ function platformFromTag(tagName) {
if (tagName.indexOf('ios') >= 0) return 'ios';
if (tagName.indexOf('clipper') === 0) return 'clipper';
if (tagName.indexOf('cli') === 0) return 'cli';
if (tagName.indexOf('plugin-generator') === 0) return 'plugin-generator';
throw new Error(`Could not determine platform from tag: ${tagName}`);
}

View File

@@ -26,8 +26,8 @@ With plugins you can directly modify Joplin by adding new features to the applic
- Create a module to export or import data into Joplin
- Define new settings and setting sections, and get/set them from the plugin
- Create a new Markdown plugin to render custom markup.
- Create an editor plugin to modify low-level the behaviour of the Markdown editor (CodeMirror)
- Create an editor plugin to modify, at a low-level, the behaviour of the Markdown editor (CodeMirror)
To get started with the plugin API, check the [Get Started](https://github.com/laurent22/joplin/blob/dev/readme/api/get_started/plugins.md) page or have a look at the [TOC tutorial](https://github.com/laurent22/joplin/blob/dev/readme/api/tutorials/toc_plugin.md).
Once you are familiar with the API, you can have a look at the [plugin API reference](https://github.com/laurent22/joplin/blob/dev/readme/api/get_started/plugins.md) for a detailed documentation about each supported feature.
Once you are familiar with the API, you can have a look at the [plugin API reference](https://joplinapp.org/api/references/plugin_api/classes/joplin.html) for a detailed documentation about each supported feature.