You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-29 23:48:19 +02:00
Compare commits
3 Commits
v1.4.12
...
plugin_gui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28be0dbd36 | ||
|
|
cd406cba51 | ||
|
|
57c0305ad8 |
@@ -151,9 +151,12 @@ packages/app-cli/app/LinkSelector.js.map
|
||||
packages/app-cli/app/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js.map
|
||||
packages/app-cli/tests/EnexToMd.d.ts
|
||||
packages/app-cli/tests/EnexToMd.js
|
||||
packages/app-cli/tests/EnexToMd.js.map
|
||||
packages/app-cli/build/LinkSelector.d.ts
|
||||
packages/app-cli/build/LinkSelector.js
|
||||
packages/app-cli/build/LinkSelector.js.map
|
||||
packages/app-cli/build/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/build/services/plugins/PluginRunner.js
|
||||
packages/app-cli/build/services/plugins/PluginRunner.js.map
|
||||
packages/app-cli/tests/InMemoryCache.d.ts
|
||||
packages/app-cli/tests/InMemoryCache.js
|
||||
packages/app-cli/tests/InMemoryCache.js.map
|
||||
@@ -1348,9 +1351,6 @@ 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
|
||||
@@ -1360,12 +1360,6 @@ 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
|
||||
@@ -1399,9 +1393,6 @@ 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
|
||||
@@ -1411,7 +1402,4 @@ 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
|
||||
|
||||
@@ -15,8 +15,7 @@ module.exports = {
|
||||
'Atomics': 'readonly',
|
||||
'SharedArrayBuffer': 'readonly',
|
||||
|
||||
// Jest variables
|
||||
'test': 'readonly',
|
||||
// Jasmine variables
|
||||
'expect': 'readonly',
|
||||
'describe': 'readonly',
|
||||
'it': 'readonly',
|
||||
@@ -24,6 +23,10 @@ module.exports = {
|
||||
'afterAll': 'readonly',
|
||||
'beforeEach': 'readonly',
|
||||
'afterEach': 'readonly',
|
||||
'jasmine': 'readonly',
|
||||
|
||||
// Jest variables
|
||||
'test': 'readonly',
|
||||
|
||||
// React Native variables
|
||||
'__DEV__': 'readonly',
|
||||
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -143,9 +143,12 @@ packages/app-cli/app/LinkSelector.js.map
|
||||
packages/app-cli/app/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js.map
|
||||
packages/app-cli/tests/EnexToMd.d.ts
|
||||
packages/app-cli/tests/EnexToMd.js
|
||||
packages/app-cli/tests/EnexToMd.js.map
|
||||
packages/app-cli/build/LinkSelector.d.ts
|
||||
packages/app-cli/build/LinkSelector.js
|
||||
packages/app-cli/build/LinkSelector.js.map
|
||||
packages/app-cli/build/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/build/services/plugins/PluginRunner.js
|
||||
packages/app-cli/build/services/plugins/PluginRunner.js.map
|
||||
packages/app-cli/tests/InMemoryCache.d.ts
|
||||
packages/app-cli/tests/InMemoryCache.js
|
||||
packages/app-cli/tests/InMemoryCache.js.map
|
||||
@@ -1340,9 +1343,6 @@ 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
|
||||
@@ -1352,12 +1352,6 @@ 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
|
||||
@@ -1391,9 +1385,6 @@ 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
|
||||
@@ -1403,7 +1394,4 @@ 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
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -1227,6 +1227,12 @@
|
||||
"@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",
|
||||
@@ -6433,6 +6439,22 @@
|
||||
"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",
|
||||
|
||||
16
package.json
16
package.json
@@ -7,12 +7,6 @@
|
||||
},
|
||||
"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/",
|
||||
@@ -32,11 +26,11 @@
|
||||
"releaseDesktop": "node packages/tools/release-electron.js",
|
||||
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
|
||||
"setupNewRelease": "node ./packages/tools/setupNewRelease",
|
||||
"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",
|
||||
"tsc": "lerna run tsc --stream --parallel",
|
||||
"test": "lerna run test --stream",
|
||||
"test-ci": "lerna run test-ci --stream",
|
||||
"updateIgnored": "gulp updateIgnoredTypeScriptBuild",
|
||||
"watch": "lerna run watch --stream --parallel"
|
||||
},
|
||||
"husky": {
|
||||
@@ -45,6 +39,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jasmine": "^3.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||
"@typescript-eslint/parser": "^4.6.0",
|
||||
"eslint": "^7.6.0",
|
||||
@@ -55,6 +50,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,12 +30,12 @@ module.exports = {
|
||||
],
|
||||
|
||||
testPathIgnorePatterns: [
|
||||
'<rootDir>/node_modules/',
|
||||
'<rootDir>/tests/support/',
|
||||
'<rootDir>/build/',
|
||||
'<rootDir>/tests/test-utils.js',
|
||||
'<rootDir>/tests/file_api_driver.js',
|
||||
'<rootDir>/tests/tmp/',
|
||||
'/node_modules/',
|
||||
'/tests\\/support/',
|
||||
'/build/',
|
||||
'test-utils.js',
|
||||
'file_api_driver.js',
|
||||
'/tests\\/tmp/',
|
||||
],
|
||||
|
||||
// To avoid this warning:
|
||||
@@ -52,5 +52,5 @@ module.exports = {
|
||||
],
|
||||
|
||||
testEnvironment: 'node',
|
||||
setupFilesAfterEnv: [`${__dirname}/jest.setup.js`],
|
||||
setupFilesAfterEnv: ['./jest.setup.js'],
|
||||
};
|
||||
|
||||
46
packages/app-cli/package-lock.json
generated
46
packages/app-cli/package-lock.json
generated
@@ -898,15 +898,11 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@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/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/node": {
|
||||
"version": "14.14.6",
|
||||
@@ -4515,6 +4511,38 @@
|
||||
"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",
|
||||
|
||||
@@ -66,9 +66,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "^1.0.9",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/jasmine": "^3.6.0",
|
||||
"@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"
|
||||
|
||||
67
packages/app-cli/tests/EnexToMd.js
Normal file
67
packages/app-cli/tests/EnexToMd.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
|
||||
const os = require('os');
|
||||
const time = require('@joplin/lib/time').default;
|
||||
const { filename } = require('@joplin/lib/path-utils');
|
||||
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
|
||||
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 { enexXmlToMd } = require('@joplin/lib/import-enex-md-gen.js');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('EnexToMd', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should convert from Enex to Markdown', asyncTest(async () => {
|
||||
const basePath = `${__dirname}/enex_to_md`;
|
||||
const files = await shim.fsDriver().readDirStats(basePath);
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const htmlFilename = files[i].path;
|
||||
if (htmlFilename.indexOf('.html') < 0) continue;
|
||||
|
||||
const htmlPath = `${basePath}/${htmlFilename}`;
|
||||
const mdPath = `${basePath}/${filename(htmlFilename)}.md`;
|
||||
|
||||
// if (htmlFilename !== 'multiline_inner_text.html') continue;
|
||||
|
||||
const html = await shim.fsDriver().readFile(htmlPath);
|
||||
let expectedMd = await shim.fsDriver().readFile(mdPath);
|
||||
|
||||
let actualMd = await enexXmlToMd(`<div>${html}</div>`, []);
|
||||
|
||||
if (os.EOL === '\r\n') {
|
||||
expectedMd = expectedMd.replace(/\r\n/g, '\n');
|
||||
actualMd = actualMd.replace(/\r\n/g, '\n');
|
||||
}
|
||||
|
||||
if (actualMd !== expectedMd) {
|
||||
console.info('');
|
||||
console.info(`Error converting file: ${htmlFilename}`);
|
||||
console.info('--------------------------------- Got:');
|
||||
console.info(actualMd.split('\n'));
|
||||
console.info('--------------------------------- Expected:');
|
||||
console.info(expectedMd.split('\n'));
|
||||
console.info('--------------------------------------------');
|
||||
console.info('');
|
||||
|
||||
expect(false).toBe(true);
|
||||
// return;
|
||||
} else {
|
||||
expect(true).toBe(true);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
import { NoteEntity, ResourceEntity, TagEntity } from '@joplin/lib/services/database/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const os = require('os');
|
||||
const { filename } = require('@joplin/lib/path-utils');
|
||||
const { setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
|
||||
const { enexXmlToMd } = require('@joplin/lib/import-enex-md-gen.js');
|
||||
const { importEnex } = require('@joplin/lib/import-enex');
|
||||
const Note = require('@joplin/lib/models/Note');
|
||||
const Tag = require('@joplin/lib/models/Tag');
|
||||
const Resource = require('@joplin/lib/models/Resource');
|
||||
|
||||
const enexSampleBaseDir = `${__dirname}/enex_to_md`;
|
||||
|
||||
describe('EnexToMd', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should convert ENEX content to Markdown', async () => {
|
||||
const files = await shim.fsDriver().readDirStats(enexSampleBaseDir);
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const htmlFilename = files[i].path;
|
||||
if (htmlFilename.indexOf('.html') < 0) continue;
|
||||
|
||||
const htmlPath = `${enexSampleBaseDir}/${htmlFilename}`;
|
||||
const mdPath = `${enexSampleBaseDir}/${filename(htmlFilename)}.md`;
|
||||
|
||||
// if (htmlFilename !== 'multiline_inner_text.html') continue;
|
||||
|
||||
const html = await shim.fsDriver().readFile(htmlPath);
|
||||
let expectedMd = await shim.fsDriver().readFile(mdPath);
|
||||
|
||||
let actualMd = await enexXmlToMd(`<div>${html}</div>`, []);
|
||||
|
||||
if (os.EOL === '\r\n') {
|
||||
expectedMd = expectedMd.replace(/\r\n/g, '\n');
|
||||
actualMd = actualMd.replace(/\r\n/g, '\n');
|
||||
}
|
||||
|
||||
if (actualMd !== expectedMd) {
|
||||
console.info('');
|
||||
console.info(`Error converting file: ${htmlFilename}`);
|
||||
console.info('--------------------------------- Got:');
|
||||
console.info(actualMd.split('\n'));
|
||||
console.info('--------------------------------- Expected:');
|
||||
console.info(expectedMd.split('\n'));
|
||||
console.info('--------------------------------------------');
|
||||
console.info('');
|
||||
|
||||
expect(false).toBe(true);
|
||||
// return;
|
||||
} else {
|
||||
expect(true).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should import ENEX metadata', async () => {
|
||||
const filePath = `${enexSampleBaseDir}/sample-enex.xml`;
|
||||
await importEnex('', filePath);
|
||||
|
||||
const note: NoteEntity = (await Note.all())[0];
|
||||
expect(note.title).toBe('Test Note for Export');
|
||||
expect(note.body).toBe([
|
||||
' Hello, World.',
|
||||
'',
|
||||
'',
|
||||
].join('\n'));
|
||||
expect(note.created_time).toBe(1375217524000);
|
||||
expect(note.updated_time).toBe(1376560800000);
|
||||
expect(note.latitude).toBe('33.88394692');
|
||||
expect(note.longitude).toBe('-117.91913551');
|
||||
expect(note.altitude).toBe('96.0000');
|
||||
expect(note.author).toBe('Brett Kelly');
|
||||
|
||||
const tag: TagEntity = (await Tag.tagsByNoteId(note.id))[0];
|
||||
expect(tag.title).toBe('fake-tag');
|
||||
|
||||
const resource: ResourceEntity = (await Resource.all())[0];
|
||||
expect(resource.id).toBe('3d0f4d01abc02cf8c4dc1c796df8c4b2');
|
||||
const stat = await fs.stat(Resource.fullPath(resource));
|
||||
expect(stat.size).toBe(277);
|
||||
});
|
||||
|
||||
it('should handle invalid dates', async () => {
|
||||
const filePath = `${enexSampleBaseDir}/invalid_date.enex`;
|
||||
await importEnex('', filePath);
|
||||
const note: NoteEntity = (await Note.all())[0];
|
||||
expect(note.created_time).toBe(1521822724000); // 20180323T163204Z
|
||||
expect(note.updated_time).toBe(1521822724000); // Because this date was invalid, it is set to the created time instead
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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').default;
|
||||
const HtmlToHtml = require('@joplin/renderer/HtmlToHtml');
|
||||
const { enexXmlToMd } = require('@joplin/lib/import-enex-md-gen.js');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
|
||||
@@ -147,27 +147,27 @@ describe('MdToHtml', function() {
|
||||
expect(result.html.trim()).toBe('<div id="rendered-md"><p>just <strong>testing</strong></p>\n</div>');
|
||||
}));
|
||||
|
||||
// it('should render links correctly', asyncTest(async () => {
|
||||
// const mdToHtml = newTestMdToHtml();
|
||||
it('should render links correctly', asyncTest(async () => {
|
||||
const mdToHtml = newTestMdToHtml();
|
||||
|
||||
// const testCases = [
|
||||
// // None of these should result in a link
|
||||
// ['https://example.com', 'https://example.com'],
|
||||
// ['file://C:\\AUTOEXEC.BAT', 'file://C:\\AUTOEXEC.BAT'],
|
||||
// ['example.com', 'example.com'],
|
||||
// ['oo.ps', 'oo.ps'],
|
||||
// ['test@example.com', 'test@example.com'],
|
||||
const testCases = [
|
||||
// None of these should result in a link
|
||||
['https://example.com', 'https://example.com'],
|
||||
['file://C:\\AUTOEXEC.BAT', 'file://C:\\AUTOEXEC.BAT'],
|
||||
['example.com', 'example.com'],
|
||||
['oo.ps', 'oo.ps'],
|
||||
['test@example.com', 'test@example.com'],
|
||||
|
||||
// // Those should be converted to links
|
||||
// ['<https://example.com>', '<a data-from-md title=\'https://example.com\' href=\'https://example.com\'>https://example.com</a>'],
|
||||
// ['[ok](https://example.com)', '<a data-from-md title=\'https://example.com\' href=\'https://example.com\'>ok</a>'],
|
||||
// ];
|
||||
// Those should be converted to links
|
||||
['<https://example.com>', '<a data-from-md title=\'https://example.com\' href=\'https://example.com\'>https://example.com</a>'],
|
||||
['[ok](https://example.com)', '<a data-from-md title=\'https://example.com\' href=\'https://example.com\'>ok</a>'],
|
||||
];
|
||||
|
||||
// for (const testCase of testCases) {
|
||||
// const [input, expected] = testCase;
|
||||
// const actual = await mdToHtml.render(input, null, { bodyOnly: true, plainResourceRendering: true });
|
||||
// expect(actual.html).toBe(expected);
|
||||
// }
|
||||
// }));
|
||||
for (const testCase of testCases) {
|
||||
const [input, expected] = testCase;
|
||||
const actual = await mdToHtml.render(input, null, { bodyOnly: true, plainResourceRendering: true });
|
||||
expect(actual.html).toBe(expected);
|
||||
}
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export3.dtd">
|
||||
<en-export export-date="20201223T053642Z" application="Evernote" version="10.4.3">
|
||||
<note>
|
||||
<title>Fruit Tree Assessment</title>
|
||||
<created>20180323T163204Z</created>
|
||||
<updated>10101T000000Z</updated>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"
|
||||
><en-note>Fruit Tree</en-note> ]]>
|
||||
</content>
|
||||
</note>
|
||||
</en-export>
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export3.dtd">
|
||||
<en-export export-date="20130730T205637Z" application="Evernote" version="Evernote Mac">
|
||||
<note>
|
||||
<title>Test Note for Export</title>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
|
||||
<en-note style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;">
|
||||
Hello, World.
|
||||
<div>
|
||||
<br/>
|
||||
</div>
|
||||
<div>
|
||||
<en-media alt="" type="image/jpeg" hash="dd7b6d285d09ec054e8cd6a3814ce093"/>
|
||||
</div>
|
||||
<div>
|
||||
<br/>
|
||||
</div>
|
||||
</en-note>
|
||||
]]>
|
||||
</content>
|
||||
<created>20130730T205204Z</created>
|
||||
<updated>20130815T100000Z</updated>
|
||||
<tag>fake-tag</tag>
|
||||
<note-attributes>
|
||||
<latitude>33.88394692352314</latitude>
|
||||
<longitude>-117.9191355110099</longitude>
|
||||
<altitude>96</altitude>
|
||||
<author>Brett Kelly</author>
|
||||
</note-attributes>
|
||||
<resource>
|
||||
<data encoding="base64">/9j/4AAQSkZJRgABAQAAAQABAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZ
|
||||
WiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQ
|
||||
kfeIGT/+uufk8DpM0gyVjGfmzkgetesnUoTHJ+5Cxn86zmv4/wB75EW+QHAPUH/P9Ky+s1rtrr/wfvOm
|
||||
dBSamnq/xPKp/hpLKmS7x4OBjgn6elee6v4OuLJirRSHb/FtyG9s9u1fR0+oTiIRvGq7W4bpisfUGk1C
|
||||
GVWtkIyM57n1rfDY+uqigtU76ffZkUsA6iajHZ6v/P8A4B//2Q==</data>
|
||||
<mime>image/jpeg</mime>
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<resource-attributes>
|
||||
<file-name>snapshot-DAE9FC15-88E3-46CF-B744-DA9B1B56EB57.jpg</file-name>
|
||||
</resource-attributes>
|
||||
</resource>
|
||||
</note>
|
||||
</en-export>
|
||||
|
||||
@@ -105,11 +105,7 @@ describe('models_BaseItem', function() {
|
||||
}));
|
||||
|
||||
it('should serialize and unserialize properties that contain new lines', asyncTest(async () => {
|
||||
const sourceUrl = `
|
||||
https://joplinapp.org/ \\n
|
||||
`;
|
||||
|
||||
const note = await Note.save({ title: 'note', source_url: sourceUrl });
|
||||
const note = await Note.save({ title: 'note', source_url: '\nhttps://joplinapp.org/\n' });
|
||||
|
||||
const noteBefore = await Note.load(note.id);
|
||||
const serialized = await Note.serialize(noteBefore);
|
||||
@@ -117,18 +113,4 @@ https://joplinapp.org/ \\n
|
||||
|
||||
expect(noteAfter).toEqual(noteBefore);
|
||||
}));
|
||||
|
||||
it('should not serialize the note title and body', asyncTest(async () => {
|
||||
const note = await Note.save({ title: 'my note', body: `one line
|
||||
two line
|
||||
three line \\n no escape` });
|
||||
|
||||
const noteBefore = await Note.load(note.id);
|
||||
const serialized = await Note.serialize(noteBefore);
|
||||
expect(serialized.indexOf(`my note
|
||||
|
||||
one line
|
||||
two line
|
||||
three line \\n no escape`)).toBe(0);
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
expect(internalToExternal).toBe(expected, 'replaceResourceInternalToExternalLinks failed');
|
||||
|
||||
const externalToInternal = await Note.replaceResourceExternalToInternalLinks(internalToExternal, { useAbsolutePaths });
|
||||
expect(externalToInternal).toBe(input);
|
||||
expect(externalToInternal).toBe(input, 'replaceResourceExternalToInternalLinks failed');
|
||||
}
|
||||
|
||||
const result = await Note.replaceResourceExternalToInternalLinks(`[](joplin://${note1.id})`);
|
||||
expect(result).toBe(`[](:/${note1.id})`);
|
||||
expect(result).toBe(`[](:/${note1.id})`, 'replaceResourceExternalToInternalLinks failed (note link)');
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
expect(model1[f]).toBe(model2[f], `For key ${f}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ 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 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');
|
||||
@@ -262,23 +260,4 @@ describe('services_PluginService', function() {
|
||||
}
|
||||
}));
|
||||
|
||||
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);
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
@@ -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, restoreDate } = require('./test-utils.js');
|
||||
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, mockDate, 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');
|
||||
|
||||
@@ -23,6 +23,12 @@ 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);
|
||||
|
||||
@@ -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(false);
|
||||
expect(r3.has_more).toBe(undefined);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -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(false);
|
||||
expect(r2.has_more).toBe(undefined);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -208,13 +208,6 @@ export enum MenuItemLocation {
|
||||
* @deprecated Do not use - same as NoteListContextMenu
|
||||
*/
|
||||
Context = 'context',
|
||||
|
||||
/**
|
||||
* The context menu that appears when right-clicking on the note
|
||||
* list, or when multiple notes are selected. Any command triggered from
|
||||
* this location will receive a `noteIds` array with the list of notes that
|
||||
* were right-clicked or selected.
|
||||
*/
|
||||
NoteListContextMenu = 'noteListContextMenu',
|
||||
EditorContextMenu = 'editorContextMenu',
|
||||
}
|
||||
|
||||
@@ -21,20 +21,6 @@ joplin.plugins.register({
|
||||
},
|
||||
});
|
||||
|
||||
await joplin.commands.register({
|
||||
name: 'contextMenuCommandExample',
|
||||
label: 'My Context Menu command',
|
||||
execute: async (noteIds:string[]) => {
|
||||
const notes = [];
|
||||
for (const noteId of noteIds) {
|
||||
notes.push(await joplin.data.get(['notes', noteId]));
|
||||
}
|
||||
|
||||
const noteTitles = notes.map((note:any) => note.title);
|
||||
alert('The following notes will be processed:\n\n' + noteTitles.join(', '));
|
||||
},
|
||||
});
|
||||
|
||||
// Commands that return a result and take argument can only be used
|
||||
// programmatically, so it's not necessary to set a label and icon.
|
||||
await joplin.commands.register({
|
||||
@@ -54,8 +40,6 @@ joplin.plugins.register({
|
||||
await joplin.views.menuItems.create('myMenuItem1', 'testCommand1', MenuItemLocation.Tools, { accelerator: 'CmdOrCtrl+Alt+Shift+B' });
|
||||
await joplin.views.menuItems.create('myMenuItem2', 'testCommand2', MenuItemLocation.Tools);
|
||||
|
||||
await joplin.views.menuItems.create('contextMenuItem1', 'contextMenuCommandExample', MenuItemLocation.NoteListContextMenu);
|
||||
|
||||
console.info('Running command with arguments...');
|
||||
const result = await joplin.commands.execute('commandWithResult', 'abcd', 123);
|
||||
console.info('Result was: ' + result);
|
||||
|
||||
@@ -131,11 +131,9 @@ setSyncTargetName('memory');
|
||||
|
||||
const syncDir = `${__dirname}/../tests/sync`;
|
||||
|
||||
// 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;
|
||||
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');
|
||||
@@ -147,8 +145,6 @@ 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);
|
||||
@@ -216,7 +212,6 @@ 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);
|
||||
|
||||
@@ -295,11 +290,6 @@ 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_;
|
||||
|
||||
@@ -313,9 +303,6 @@ 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));
|
||||
@@ -557,7 +544,7 @@ function asyncTest(callback) {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
if (error.constructor && error.constructor.name === 'ExpectationFailed') {
|
||||
// OK - will be reported by Jest
|
||||
// OK - will be reported by Jasmine
|
||||
} else {
|
||||
// Better to rethrow exception as stack trace is more useful in this case
|
||||
throw error;
|
||||
@@ -669,17 +656,15 @@ async function createTempDir() {
|
||||
return tempDirPath;
|
||||
}
|
||||
|
||||
// TODO: Update for Jest
|
||||
function mockDate(year, month, day, tick) {
|
||||
const fixedDate = new Date(2020, 0, 1);
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(fixedDate);
|
||||
}
|
||||
|
||||
// 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();
|
||||
// }
|
||||
function restoreDate() {
|
||||
jasmine.clock().uninstall();
|
||||
}
|
||||
|
||||
// Application for feature integration testing
|
||||
class TestApp extends BaseApplication {
|
||||
@@ -736,7 +721,7 @@ class TestApp extends BaseApplication {
|
||||
}
|
||||
|
||||
async profileDir() {
|
||||
return Setting.value('profileDir');
|
||||
return await Setting.value('profileDir');
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
@@ -749,4 +734,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, 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, mockDate, restoreDate, TestApp };
|
||||
|
||||
@@ -87,7 +87,6 @@ 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
|
||||
|
||||
@@ -490,9 +490,15 @@ 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();
|
||||
service.setLogger(pluginLogger);
|
||||
service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
|
||||
|
||||
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||
|
||||
@@ -119,7 +119,7 @@ export class Bridge {
|
||||
showConfirmMessageBox(message: string, options: any = null) {
|
||||
options = {
|
||||
buttons: [_('OK'), _('Cancel')],
|
||||
...options,
|
||||
...options
|
||||
};
|
||||
|
||||
const result = this.showMessageBox_(this.window(), Object.assign({}, {
|
||||
@@ -170,8 +170,8 @@ export class Bridge {
|
||||
return require('electron').shell.openExternal(url);
|
||||
}
|
||||
|
||||
async openItem(fullPath: string) {
|
||||
return require('electron').shell.openPath(fullPath);
|
||||
openItem(fullPath: string) {
|
||||
return require('electron').shell.openItem(fullPath);
|
||||
}
|
||||
|
||||
checkForUpdates(inBackground: boolean, window: any, logFilePath: string, options: any) {
|
||||
|
||||
@@ -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 { clipboard } = require('electron');
|
||||
const bridge = require('electron').remote.require('./bridge').default;
|
||||
const { shell, 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: () => {
|
||||
bridge().openItem(Setting.value('templateDir'));
|
||||
shell.openItem(Setting.value('templateDir'));
|
||||
},
|
||||
}, {
|
||||
label: _('Refresh templates'),
|
||||
|
||||
@@ -22,7 +22,6 @@ 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');
|
||||
@@ -256,6 +255,54 @@ 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');
|
||||
@@ -362,12 +409,6 @@ 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;
|
||||
}
|
||||
@@ -582,91 +623,6 @@ 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 (
|
||||
@@ -684,6 +640,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
plugins={props.plugins}
|
||||
onChange={codeMirror_change}
|
||||
onScroll={editor_scroll}
|
||||
onEditorContextMenu={onEditorContextMenu}
|
||||
onEditorPaste={onEditorPaste}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -27,8 +27,6 @@ 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');
|
||||
@@ -79,18 +77,19 @@ 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;
|
||||
onEditorPaste: any;
|
||||
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,
|
||||
}
|
||||
|
||||
function Editor(props: EditorProps, ref: any) {
|
||||
@@ -123,6 +122,13 @@ 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();
|
||||
@@ -157,7 +163,7 @@ function Editor(props: EditorProps, ref: any) {
|
||||
mode: props.mode,
|
||||
readOnly: props.readOnly,
|
||||
autoCloseBrackets: props.autoMatchBraces,
|
||||
inputStyle: Setting.value('editor.spellcheckBeta') ? 'contenteditable' : 'textarea',
|
||||
inputStyle: 'textarea', // contenteditable loses cursor position on focus change, use textarea instead
|
||||
lineWrapping: true,
|
||||
lineNumbers: false,
|
||||
indentWithTabs: true,
|
||||
@@ -171,6 +177,7 @@ 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);
|
||||
@@ -184,6 +191,7 @@ 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);
|
||||
@@ -242,7 +250,7 @@ function Editor(props: EditorProps, ref: any) {
|
||||
}
|
||||
}, [pluginOptions, editor]);
|
||||
|
||||
return <div className='codeMirrorEditor' style={props.style} ref={editorParent} />;
|
||||
return <div style={props.style} ref={editorParent} />;
|
||||
}
|
||||
|
||||
export default forwardRef(Editor);
|
||||
|
||||
@@ -100,33 +100,5 @@ 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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -3,16 +3,15 @@ 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;
|
||||
|
||||
@@ -15,6 +15,7 @@ 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');
|
||||
|
||||
@@ -41,14 +41,14 @@ export default function styles(props: Props) {
|
||||
leftIcon: {
|
||||
fontSize: iconSize,
|
||||
position: 'relative',
|
||||
top: 1,
|
||||
top: 2,
|
||||
color: theme.color3,
|
||||
},
|
||||
rightIcon: {
|
||||
fontSize: iconSize - 1,
|
||||
borderLeft: 'none',
|
||||
position: 'relative',
|
||||
top: 1,
|
||||
top: 2,
|
||||
color: theme.color3,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,7 +40,6 @@ 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}`;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const smalltalk = require('smalltalk');
|
||||
|
||||
class Dialogs {
|
||||
async alert(message: string, title = '') {
|
||||
async alert(message:string, title = '') {
|
||||
await smalltalk.alert(title, message);
|
||||
}
|
||||
|
||||
async confirm(message: string, title = '', options: any = {}) {
|
||||
async confirm(message:string, title = '', options:any = {}) {
|
||||
try {
|
||||
await smalltalk.confirm(title, message, options);
|
||||
return true;
|
||||
@@ -14,7 +14,7 @@ class Dialogs {
|
||||
}
|
||||
}
|
||||
|
||||
async prompt(message: string, title = '', defaultValue = '', options: any = null) {
|
||||
async prompt(message:string, title = '', defaultValue = '', options:any = null) {
|
||||
if (options === null) options = {};
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
export default function(props:Props) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
return (
|
||||
@@ -18,10 +18,10 @@ export default function(props: Props) {
|
||||
onToggle={props.onToggle}
|
||||
colors={{
|
||||
activeThumb: {
|
||||
base: Color(theme.color5).rgb().string(),
|
||||
base: Color(theme.color5).rgb().string(),
|
||||
},
|
||||
active: {
|
||||
base: Color(theme.backgroundColor5).alpha(0.7).rgb().string(),
|
||||
base: Color(theme.backgroundColor5).alpha(0.7).rgb().string(),
|
||||
},
|
||||
}}
|
||||
trackStyle={{
|
||||
@@ -34,4 +34,4 @@ export default function(props: Props) {
|
||||
activeLabel=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export default class NoteListUtils {
|
||||
if (location !== MenuItemLocation.Context && location !== MenuItemLocation.NoteListContextMenu) continue;
|
||||
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem(info.view.commandName, noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem(info.view.commandName))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ 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) {
|
||||
@@ -87,7 +86,7 @@ try {
|
||||
keytar = null;
|
||||
}
|
||||
|
||||
shimInit(null, keytar, React);
|
||||
shimInit(null, keytar);
|
||||
|
||||
// Disable drag and drop of links inside application (which would
|
||||
// open it as if the whole app was a browser)
|
||||
|
||||
2104
packages/app-desktop/package-lock.json
generated
2104
packages/app-desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "1.4.12",
|
||||
"version": "1.4.10",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -11,8 +11,7 @@
|
||||
"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-ci": "test"
|
||||
"test": "jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -90,6 +89,7 @@
|
||||
"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": "^10.1.6",
|
||||
"electron-builder": "22.9.1",
|
||||
"electron-rebuild": "^2.3.2",
|
||||
"electron": "^8.2.5",
|
||||
"electron-builder": "22.3.2",
|
||||
"electron-rebuild": "^1.10.1",
|
||||
"glob": "^7.1.6",
|
||||
"gulp": "^4.0.2",
|
||||
"jest": "^26.6.3",
|
||||
|
||||
@@ -87,6 +87,7 @@ 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"
|
||||
|
||||
@@ -399,8 +399,6 @@ async function initialize(dispatch) {
|
||||
mainLogger.setLevel(Logger.LEVEL_DEBUG);
|
||||
}
|
||||
|
||||
Logger.initializeGlobalLogger(mainLogger);
|
||||
|
||||
reg.setLogger(mainLogger);
|
||||
reg.setShowErrorMessageBoxHandler((message) => { alert(message); });
|
||||
|
||||
|
||||
@@ -4,29 +4,6 @@ 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) {
|
||||
@@ -41,6 +18,8 @@ module.exports = class extends Generator {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async prompting() {
|
||||
this.log(yosay(`Welcome to the fine ${chalk.red('generator-joplin')} generator!`));
|
||||
|
||||
@@ -131,28 +110,12 @@ module.exports = class extends Generator {
|
||||
if (this.options.update && noUpdateFiles.includes(file)) continue;
|
||||
|
||||
const destFile = file.replace(/_TEMPLATE/, '');
|
||||
const destFilePath = this.destinationPath(destFile);
|
||||
|
||||
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.copyTpl(
|
||||
this.templatePath(file),
|
||||
this.destinationPath(destFile),
|
||||
this.props
|
||||
);
|
||||
}
|
||||
|
||||
this.fs.copy(
|
||||
|
||||
@@ -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,7 +68,10 @@ 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);
|
||||
}
|
||||
|
||||
@@ -98,13 +101,15 @@ export default class BaseApplication {
|
||||
EncryptionService.instance_ = null;
|
||||
DecryptionWorker.instance_ = null;
|
||||
|
||||
appLogger.info('Base application terminated...');
|
||||
this.logger_.info('Base application terminated...');
|
||||
this.logger_ = null;
|
||||
this.dbLogger_ = null;
|
||||
this.eventEmitter_ = null;
|
||||
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = null;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return appLogger;
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public store() {
|
||||
@@ -284,7 +289,7 @@ export default class BaseApplication {
|
||||
parentType = BaseModel.TYPE_SMART_FILTER;
|
||||
}
|
||||
|
||||
appLogger.debug('Refreshing notes:', parentType, parentId);
|
||||
this.logger().debug('Refreshing notes:', parentType, parentId);
|
||||
|
||||
const options = {
|
||||
order: stateUtils.notesOrder(state.settings),
|
||||
@@ -455,7 +460,7 @@ export default class BaseApplication {
|
||||
}
|
||||
|
||||
async generalMiddleware(store: any, next: any, action: any) {
|
||||
// appLogger.debug('Reducer action', this.reducerActionToString(action));
|
||||
// this.logger().debug('Reducer action', this.reducerActionToString(action));
|
||||
|
||||
const result = next(action);
|
||||
const newState = store.getState();
|
||||
@@ -542,11 +547,7 @@ export default class BaseApplication {
|
||||
}
|
||||
|
||||
if (action.type === 'NOTE_UPDATE_ONE') {
|
||||
if (!action.changedFields.length ||
|
||||
action.changedFields.includes('parent_id') ||
|
||||
action.changedFields.includes('encryption_applied') ||
|
||||
action.changedFields.includes('is_conflict')
|
||||
) {
|
||||
if (!action.changedFields.length || action.changedFields.includes('parent_id') || action.changedFields.includes('encryption_applied')) {
|
||||
refreshFolders = true;
|
||||
}
|
||||
}
|
||||
@@ -700,30 +701,35 @@ export default class BaseApplication {
|
||||
const extraFlags = await this.readFlagsFromFile(`${profileDir}/flags.txt`);
|
||||
initArgs = Object.assign(initArgs, extraFlags);
|
||||
|
||||
|
||||
|
||||
|
||||
const globalLogger = new Logger();
|
||||
globalLogger.addTarget(TargetType.File, { path: `${profileDir}/log.txt` });
|
||||
if (Setting.value('appType') === 'desktop') {
|
||||
globalLogger.addTarget(TargetType.Console);
|
||||
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 });
|
||||
}
|
||||
globalLogger.setLevel(initArgs.logLevel);
|
||||
Logger.initializeGlobalLogger(globalLogger);
|
||||
this.logger_.setLevel(initArgs.logLevel);
|
||||
|
||||
|
||||
|
||||
reg.setLogger(Logger.create(''));
|
||||
reg.setLogger(this.logger_);
|
||||
reg.dispatch = () => {};
|
||||
|
||||
BaseService.logger_ = globalLogger;
|
||||
BaseService.logger_ = this.logger_;
|
||||
// require('lib/ntpDate').setLogger(reg.logger());
|
||||
|
||||
this.dbLogger_.addTarget(TargetType.File, { path: `${profileDir}/log-database.txt` });
|
||||
this.dbLogger_.setLevel(initArgs.logLevel);
|
||||
|
||||
appLogger.info(`Profile directory: ${profileDir}`);
|
||||
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}`);
|
||||
|
||||
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
|
||||
this.database_.setLogExcludedQueryTypes(['SELECT']);
|
||||
this.database_.setLogger(globalLogger);
|
||||
this.database_.setLogger(this.dbLogger_);
|
||||
|
||||
// if (Setting.value('env') === 'dev') {
|
||||
// if (shim.isElectron()) {
|
||||
@@ -750,7 +756,7 @@ export default class BaseApplication {
|
||||
|
||||
await loadKeychainServiceAndSettings(KeychainServiceDriver);
|
||||
|
||||
appLogger.info(`Client ID: ${Setting.value('clientId')}`);
|
||||
this.logger_.info(`Client ID: ${Setting.value('clientId')}`);
|
||||
|
||||
if (Setting.value('firstStart')) {
|
||||
const locale = shim.detectAndSetLocale(Setting);
|
||||
@@ -811,9 +817,9 @@ export default class BaseApplication {
|
||||
|
||||
KvStore.instance().setDb(reg.db());
|
||||
|
||||
EncryptionService.instance().setLogger(globalLogger);
|
||||
EncryptionService.instance().setLogger(this.logger_);
|
||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||
DecryptionWorker.instance().setLogger(globalLogger);
|
||||
DecryptionWorker.instance().setLogger(this.logger_);
|
||||
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
|
||||
DecryptionWorker.instance().setKvStore(KvStore.instance());
|
||||
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||
@@ -822,7 +828,7 @@ export default class BaseApplication {
|
||||
ResourceFetcher.instance().setFileApi(() => {
|
||||
return reg.syncTarget().fileApi();
|
||||
});
|
||||
ResourceFetcher.instance().setLogger(globalLogger);
|
||||
ResourceFetcher.instance().setLogger(this.logger_);
|
||||
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
|
||||
ResourceFetcher.instance().start();
|
||||
|
||||
|
||||
@@ -26,13 +26,6 @@ interface Target {
|
||||
source?: string;
|
||||
}
|
||||
|
||||
interface LoggerWrapper {
|
||||
debug: Function;
|
||||
info: Function;
|
||||
warn: Function;
|
||||
error: Function;
|
||||
}
|
||||
|
||||
class Logger {
|
||||
|
||||
// For backward compatibility
|
||||
@@ -43,7 +36,6 @@ 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;
|
||||
@@ -54,24 +46,6 @@ 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;
|
||||
}
|
||||
@@ -158,12 +132,14 @@ class Logger {
|
||||
return this.level();
|
||||
}
|
||||
|
||||
public log(level: LogLevel, prefix: string, ...object: any[]) {
|
||||
log(level: LogLevel, ...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;
|
||||
|
||||
@@ -173,30 +149,26 @@ class Logger {
|
||||
if (level == LogLevel.Warn) fn = 'warn';
|
||||
if (level == LogLevel.Info) fn = 'info';
|
||||
const consoleObj = target.console ? target.console : console;
|
||||
const prefixItems = [moment().format('HH:mm:ss')];
|
||||
if (targetPrefix) prefixItems.push(targetPrefix);
|
||||
const items = [`${prefixItems.join(': ')}:`].concat(...object);
|
||||
let items = [moment().format('HH:mm:ss')];
|
||||
if (target.prefix) {
|
||||
items.push(target.prefix);
|
||||
}
|
||||
items = items.concat(...object);
|
||||
consoleObj[fn](...items);
|
||||
} else if (target.type == 'file') {
|
||||
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
|
||||
const line = [timestamp];
|
||||
if (targetPrefix) line.push(targetPrefix);
|
||||
line.push(this.objectsToString(...object));
|
||||
const serializedObject = this.objectsToString(...object);
|
||||
try {
|
||||
// TODO: Should log async
|
||||
Logger.fsDriver().appendFileSync(target.path, `${line.join(': ')}\n`);
|
||||
Logger.fsDriver().appendFileSync(target.path, `${line + serializedObject}\n`);
|
||||
} catch (error) {
|
||||
console.error('Cannot write to log file:', error);
|
||||
}
|
||||
} else if (target.type == 'database') {
|
||||
const msg = [];
|
||||
if (targetPrefix) msg.push(targetPrefix);
|
||||
msg.push(this.objectsToString(...object));
|
||||
const msg = this.objectsToString(...object);
|
||||
|
||||
const queries = [
|
||||
{
|
||||
sql: 'INSERT INTO logs (`source`, `level`, `message`, `timestamp`) VALUES (?, ?, ?, ?)',
|
||||
params: [target.source, level, msg.join(': '), time.unixMs()],
|
||||
params: [target.source, level, msg, time.unixMs()],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -216,16 +188,16 @@ class Logger {
|
||||
}
|
||||
|
||||
error(...object: any[]) {
|
||||
return this.log(LogLevel.Error, null, ...object);
|
||||
return this.log(LogLevel.Error, ...object);
|
||||
}
|
||||
warn(...object: any[]) {
|
||||
return this.log(LogLevel.Warn, null, ...object);
|
||||
return this.log(LogLevel.Warn, ...object);
|
||||
}
|
||||
info(...object: any[]) {
|
||||
return this.log(LogLevel.Info, null, ...object);
|
||||
return this.log(LogLevel.Info, ...object);
|
||||
}
|
||||
debug(...object: any[]) {
|
||||
return this.log(LogLevel.Debug, null, ...object);
|
||||
return this.log(LogLevel.Debug, ...object);
|
||||
}
|
||||
|
||||
static levelStringToId(s: string) {
|
||||
|
||||
@@ -97,9 +97,9 @@ shared.scheduleSaveSettings = function(comp) {
|
||||
|
||||
shared.scheduleSaveSettingsIID = setTimeout(() => {
|
||||
shared.scheduleSaveSettingsIID = null;
|
||||
shared.saveSettings(comp);
|
||||
shared.saveSettings(comp);
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
|
||||
shared.saveSettings = function(comp) {
|
||||
for (const key in comp.state.settings) {
|
||||
|
||||
@@ -6,7 +6,6 @@ const shim = require('./shim').default;
|
||||
class Database {
|
||||
constructor(driver) {
|
||||
this.debugMode_ = false;
|
||||
this.sqlQueryLogEnabled_ = false;
|
||||
this.driver_ = driver;
|
||||
this.logger_ = new Logger();
|
||||
this.logExcludedQueryTypes_ = [];
|
||||
@@ -239,8 +238,6 @@ 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++) {
|
||||
|
||||
@@ -18,7 +18,7 @@ const shim = require('./shim').default;
|
||||
// const Promise = require('promise');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
function dateToTimestamp(s, defaultValue = null) {
|
||||
function dateToTimestamp(s, zeroIfInvalid = false) {
|
||||
// Most dates seem to be in this format
|
||||
let m = moment(s, 'YYYYMMDDTHHmmssZ');
|
||||
|
||||
@@ -27,7 +27,7 @@ function dateToTimestamp(s, defaultValue = null) {
|
||||
if (!m.isValid()) m = moment(s, 'YYYYMMDDThmmss AZ');
|
||||
|
||||
if (!m.isValid()) {
|
||||
if (defaultValue !== null) return defaultValue;
|
||||
if (zeroIfInvalid) return 0;
|
||||
throw new Error(`Invalid date: ${s}`);
|
||||
}
|
||||
|
||||
@@ -258,20 +258,6 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
if (!('onProgress' in importOptions)) importOptions.onProgress = function() {};
|
||||
if (!('onError' in importOptions)) importOptions.onError = function() {};
|
||||
|
||||
const handleSaxStreamEvent = (fn) => {
|
||||
return function(...args) {
|
||||
try {
|
||||
fn(...args);
|
||||
} catch (error) {
|
||||
if (importOptions.onError) {
|
||||
importOptions.onError(error);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const progressState = {
|
||||
loaded: 0,
|
||||
@@ -351,15 +337,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
note.parent_id = parentFolderId;
|
||||
note.body = body;
|
||||
|
||||
// If the created timestamp was invalid, it would be
|
||||
// set to zero, so set it to the current date here
|
||||
if (!note.created_time) note.created_time = Date.now();
|
||||
|
||||
// Notes in enex files always have a created timestamp
|
||||
// but not always an updated timestamp (it the note has
|
||||
// never been modified). For sync we require an
|
||||
// updated_time property, so set it to create_time in
|
||||
// that case
|
||||
// Notes in enex files always have a created timestamp but not always an
|
||||
// updated timestamp (it the note has never been modified). For sync
|
||||
// we require an updated_time property, so set it to create_time in that case
|
||||
if (!note.updated_time) note.updated_time = note.created_time;
|
||||
|
||||
const result = await saveNoteToStorage(note, importOptions);
|
||||
@@ -388,7 +368,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
importOptions.onError(error);
|
||||
});
|
||||
|
||||
saxStream.on('text', handleSaxStreamEvent(function(text) {
|
||||
saxStream.on('text', function(text) {
|
||||
const n = currentNodeName();
|
||||
|
||||
if (noteAttributes) {
|
||||
@@ -415,9 +395,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
if (n == 'title') {
|
||||
note.title = text;
|
||||
} else if (n == 'created') {
|
||||
note.created_time = dateToTimestamp(text, 0);
|
||||
note.created_time = dateToTimestamp(text);
|
||||
} else if (n == 'updated') {
|
||||
note.updated_time = dateToTimestamp(text, 0);
|
||||
note.updated_time = dateToTimestamp(text);
|
||||
} else if (n == 'tag') {
|
||||
note.tags.push(text);
|
||||
} else if (n == 'note') {
|
||||
@@ -428,9 +408,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
console.warn(`Unsupported note tag: ${n}`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
saxStream.on('opentag', handleSaxStreamEvent(function(node) {
|
||||
saxStream.on('opentag', function(node) {
|
||||
const n = node.name.toLowerCase();
|
||||
nodes.push(node);
|
||||
|
||||
@@ -448,9 +428,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
} else if (n == 'resource') {
|
||||
noteResource = {};
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
saxStream.on('cdata', handleSaxStreamEvent(function(data) {
|
||||
saxStream.on('cdata', function(data) {
|
||||
const n = currentNodeName();
|
||||
|
||||
if (noteResourceRecognition) {
|
||||
@@ -464,9 +444,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
saxStream.on('closetag', handleSaxStreamEvent(function(n) {
|
||||
saxStream.on('closetag', async function(n) {
|
||||
nodes.pop();
|
||||
|
||||
if (n == 'note') {
|
||||
@@ -496,9 +476,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
note.altitude = noteAttributes.altitude;
|
||||
note.author = noteAttributes.author ? noteAttributes.author.trim() : '';
|
||||
note.is_todo = noteAttributes['reminder-order'] !== '0' && !!noteAttributes['reminder-order'];
|
||||
note.todo_due = dateToTimestamp(noteAttributes['reminder-time'], 0);
|
||||
note.todo_completed = dateToTimestamp(noteAttributes['reminder-done-time'], 0);
|
||||
note.order = dateToTimestamp(noteAttributes['reminder-order'], 0);
|
||||
note.todo_due = dateToTimestamp(noteAttributes['reminder-time'], true);
|
||||
note.todo_completed = dateToTimestamp(noteAttributes['reminder-done-time'], true);
|
||||
note.order = dateToTimestamp(noteAttributes['reminder-order'], true);
|
||||
note.source = noteAttributes.source ? `evernote.${noteAttributes.source.trim()}` : 'evernote';
|
||||
note.source_url = noteAttributes['source-url'] ? noteAttributes['source-url'].trim() : '';
|
||||
|
||||
@@ -515,9 +495,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
|
||||
noteResource = null;
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
saxStream.on('end', handleSaxStreamEvent(function() {
|
||||
saxStream.on('end', function() {
|
||||
// Wait till there is no more notes to process.
|
||||
const iid = shim.setInterval(() => {
|
||||
processNotes().then(allDone => {
|
||||
@@ -527,7 +507,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}));
|
||||
});
|
||||
|
||||
stream.pipe(saxStream);
|
||||
});
|
||||
|
||||
@@ -260,13 +260,7 @@ class BaseItem extends BaseModel {
|
||||
propValue = `${propValue}`;
|
||||
}
|
||||
|
||||
if (propName === 'body') return propValue;
|
||||
|
||||
return propValue
|
||||
.replace(/\\n/g, '\\\\n')
|
||||
.replace(/\\r/g, '\\\\r')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r');
|
||||
return propValue.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
||||
}
|
||||
|
||||
static unserialize_format(type, propName, propValue) {
|
||||
@@ -287,14 +281,7 @@ class BaseItem extends BaseModel {
|
||||
propValue = Database.formatValue(ItemClass.fieldType(propName), propValue);
|
||||
}
|
||||
|
||||
if (propName === 'body') return propValue;
|
||||
|
||||
return typeof propValue === 'string' ? propValue
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\r/g, '\r')
|
||||
.replace(/\\\n/g, '\\n')
|
||||
.replace(/\\\r/g, '\\r')
|
||||
: propValue;
|
||||
return typeof propValue === 'string' ? propValue.replace(/\\n/g, '\n').replace(/\\r/g, '\r') : propValue;
|
||||
}
|
||||
|
||||
static async serialize(item, shownKeys = null) {
|
||||
|
||||
@@ -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', 'is_conflict'];
|
||||
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language'];
|
||||
|
||||
if (options.includeTimestamps) {
|
||||
output.push('updated_time');
|
||||
|
||||
@@ -44,9 +44,9 @@ export interface SettingItem {
|
||||
maximum?: number;
|
||||
step?: number;
|
||||
onClick?(): void;
|
||||
unitLabel?: Function;
|
||||
unitLabel?:Function;
|
||||
needRestart?: boolean;
|
||||
autoSave?: boolean;
|
||||
autoSave?:boolean;
|
||||
}
|
||||
|
||||
interface SettingItems {
|
||||
@@ -715,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: platform !== 'linux', appTypes: ['desktop'], label: () => _('Automatically update the application') },
|
||||
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, section: 'application', public: true, 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': {
|
||||
@@ -775,16 +775,6 @@ 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,
|
||||
@@ -839,7 +829,7 @@ class Setting extends BaseModel {
|
||||
minimum: 1,
|
||||
maximum: 365 * 2,
|
||||
step: 1,
|
||||
unitLabel: (value: number = null) => {
|
||||
unitLabel: (value:number = null) => {
|
||||
return value === null ? _('days') : _('%d days', value);
|
||||
},
|
||||
label: () => _('Keep note history for'),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Pagination, PaginationOrder } from './types';
|
||||
|
||||
export interface ModelFeedPage {
|
||||
items: any[];
|
||||
has_more: boolean;
|
||||
has_more?: boolean;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,9 @@ export default async function(db: any, tableName: string, pagination: Pagination
|
||||
|
||||
const rows = await db.selectAll(sql, sqlParams);
|
||||
|
||||
return {
|
||||
items: rows,
|
||||
has_more: rows.length >= pagination.limit,
|
||||
};
|
||||
const output: ModelFeedPage = { items: rows };
|
||||
|
||||
if (rows.length >= pagination.limit) output.has_more = true;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
202
packages/lib/package-lock.json
generated
202
packages/lib/package-lock.json
generated
@@ -98,6 +98,35 @@
|
||||
"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": {
|
||||
@@ -241,15 +270,6 @@
|
||||
"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",
|
||||
@@ -511,14 +531,6 @@
|
||||
"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",
|
||||
@@ -741,18 +753,6 @@
|
||||
"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,11 +1512,6 @@
|
||||
"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",
|
||||
@@ -1532,6 +1527,11 @@
|
||||
"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,6 +1545,21 @@
|
||||
"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": {
|
||||
@@ -1555,11 +1570,6 @@
|
||||
"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",
|
||||
@@ -1577,39 +1587,6 @@
|
||||
"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",
|
||||
@@ -1668,26 +1645,15 @@
|
||||
"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": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
|
||||
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
|
||||
"optional": true,
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
|
||||
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
|
||||
"requires": {
|
||||
"abbrev": "1"
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
@@ -1981,27 +1947,6 @@
|
||||
"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",
|
||||
@@ -2153,9 +2098,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
|
||||
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
},
|
||||
"server-destroy": {
|
||||
"version": "1.0.1",
|
||||
@@ -2223,12 +2168,11 @@
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||
},
|
||||
"sqlite3": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.0.tgz",
|
||||
"integrity": "sha512-rjvqHFUaSGnzxDy2AHCwhHy6Zp6MNJzCPGYju4kD8yi6bze4d1/zMTg6C7JI49b7/EM7jKMTvyfN/4ylBKdwfw==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz",
|
||||
"integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==",
|
||||
"requires": {
|
||||
"node-addon-api": "2.0.0",
|
||||
"node-gyp": "3.x",
|
||||
"nan": "^2.12.1",
|
||||
"node-pre-gyp": "^0.11.0"
|
||||
}
|
||||
},
|
||||
@@ -2331,21 +2275,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -2715,15 +2644,6 @@
|
||||
"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",
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"reselect": "^4.0.0",
|
||||
"server-destroy": "^1.0.1",
|
||||
"sprintf-js": "^1.1.2",
|
||||
"sqlite3": "^5.0.0",
|
||||
"sqlite3": "^4.1.1",
|
||||
"string-padding": "^1.0.2",
|
||||
"string-to-stream": "^1.1.0",
|
||||
"tar": "^4.4.10",
|
||||
|
||||
@@ -787,14 +787,8 @@ 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) {
|
||||
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
|
||||
// Note is still in the same folder
|
||||
if (isViewingAllNotes || noteIsInFolder(modNote, n.parent_id)) {
|
||||
// Merge the properties that have changed (in modNote) into
|
||||
// the object we already have.
|
||||
newNotes[i] = Object.assign({}, newNotes[i]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import AsyncActionQueue from '../../AsyncActionQueue';
|
||||
import shim from '../../shim';
|
||||
import { _ } from '../../locale';
|
||||
import { toSystemSlashes } from '../../path-utils';
|
||||
const Logger = require('../../Logger').default;
|
||||
const Setting = require('../../models/Setting').default;
|
||||
const Resource = require('../../models/Resource');
|
||||
@@ -158,8 +157,6 @@ export default class ResourceEditWatcher {
|
||||
if (!this.watcher_) {
|
||||
this.watcher_ = this.chokidar_.watch(fileToWatch);
|
||||
this.watcher_.on('all', async (event: any, path: string) => {
|
||||
path = toSystemSlashes(path, 'linux');
|
||||
|
||||
this.logger().info(`ResourceEditWatcher: Event: ${event}: ${path}`);
|
||||
|
||||
if (event === 'unlink') {
|
||||
@@ -186,13 +183,11 @@ export default class ResourceEditWatcher {
|
||||
//
|
||||
// @ts-ignore Leave unused path variable
|
||||
this.watcher_.on('raw', async (event: string, path: string, options: any) => {
|
||||
const watchedPath = toSystemSlashes(options.watchedPath, 'linux');
|
||||
|
||||
this.logger().debug(`ResourceEditWatcher: Raw event: ${event}: ${watchedPath}`);
|
||||
this.logger().debug(`ResourceEditWatcher: Raw event: ${event}: ${options.watchedPath}`);
|
||||
if (event === 'rename') {
|
||||
this.watcher_.unwatch(watchedPath);
|
||||
this.watcher_.add(watchedPath);
|
||||
handleChangeEvent(watchedPath);
|
||||
this.watcher_.unwatch(options.watchedPath);
|
||||
this.watcher_.add(options.watchedPath);
|
||||
handleChangeEvent(options.watchedPath);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -223,7 +218,7 @@ export default class ResourceEditWatcher {
|
||||
if (!(await Resource.isReady(resource))) throw new Error(_('This attachment is not downloaded or not decrypted yet'));
|
||||
const sourceFilePath = Resource.fullPath(resource);
|
||||
const tempDir = await this.tempDir();
|
||||
const editFilePath = toSystemSlashes(await shim.fsDriver().findUniqueFilename(`${tempDir}/${Resource.friendlySafeFilename(resource)}`), 'linux');
|
||||
const editFilePath = await shim.fsDriver().findUniqueFilename(`${tempDir}/${Resource.friendlySafeFilename(resource)}`);
|
||||
await shim.fsDriver().copy(sourceFilePath, editFilePath);
|
||||
const stat = await shim.fsDriver().stat(editFilePath);
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import { ContentScriptType } from './api/types';
|
||||
import Logger from '../../Logger';
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const logger = Logger.create('Plugin');
|
||||
|
||||
interface ViewControllers {
|
||||
[key: string]: ViewController;
|
||||
}
|
||||
@@ -26,16 +24,18 @@ export default class Plugin {
|
||||
private baseDir_: string;
|
||||
private manifest_: PluginManifest;
|
||||
private scriptText_: string;
|
||||
private logger_: Logger = null;
|
||||
private viewControllers_: ViewControllers = {};
|
||||
private contentScripts_: ContentScripts = {};
|
||||
private dispatch_: Function;
|
||||
private eventEmitter_: any;
|
||||
private devMode_: boolean = false;
|
||||
|
||||
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function) {
|
||||
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, logger: Logger, dispatch: Function) {
|
||||
this.baseDir_ = shim.fsDriver().resolve(baseDir);
|
||||
this.manifest_ = manifest;
|
||||
this.scriptText_ = scriptText;
|
||||
this.logger_ = logger;
|
||||
this.dispatch_ = dispatch;
|
||||
this.eventEmitter_ = new EventEmitter();
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export default class Plugin {
|
||||
|
||||
this.contentScripts_[type].push({ id, path: absolutePath });
|
||||
|
||||
logger.debug(`"${this.id}": Registered content script: ${type}: ${id}: ${absolutePath}`);
|
||||
this.logger_.debug(`Plugin: ${this.id}: Registered content script: ${type}: ${id}: ${absolutePath}`);
|
||||
|
||||
this.dispatch_({
|
||||
type: 'PLUGIN_CONTENT_SCRIPTS_ADD',
|
||||
@@ -117,7 +117,7 @@ export default class Plugin {
|
||||
}
|
||||
|
||||
public deprecationNotice(goneInVersion: string, message: string) {
|
||||
logger.warn(`"${this.id}": DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`);
|
||||
this.logger_.warn(`Plugin: ${this.id}: DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,15 +4,12 @@ import Global from './api/Global';
|
||||
import BasePluginRunner from './BasePluginRunner';
|
||||
import BaseService from '../BaseService';
|
||||
import shim from '../../shim';
|
||||
import { filename, dirname, rtrimSlashes } from '../../path-utils';
|
||||
import { filename, dirname, rtrimSlashes, basename } from '../../path-utils';
|
||||
import Setting from '../../models/Setting';
|
||||
import Logger from '../../Logger';
|
||||
const compareVersions = require('compare-versions');
|
||||
const uslug = require('uslug');
|
||||
const md5File = require('md5-file/promise');
|
||||
|
||||
const logger = Logger.create('PluginService');
|
||||
|
||||
// Plugin data is split into two:
|
||||
//
|
||||
// - First there's the service `plugins` property, which contains the
|
||||
@@ -216,7 +213,7 @@ export default class PluginService extends BaseService {
|
||||
distPath = `${path}/dist`;
|
||||
}
|
||||
|
||||
logger.info(`Loading plugin from ${path}`);
|
||||
this.logger().info(`PluginService: Loading plugin from ${path}`);
|
||||
|
||||
const scriptText = await fsDriver.readFile(`${distPath}/index.js`);
|
||||
const manifestText = await fsDriver.readFile(`${distPath}/manifest.json`);
|
||||
@@ -245,17 +242,12 @@ export default class PluginService extends BaseService {
|
||||
|
||||
const manifest = manifestFromObject(manifestObj);
|
||||
|
||||
const plugin = new Plugin(baseDir, manifest, scriptText, (action: any) => this.store_.dispatch(action));
|
||||
const plugin = new Plugin(baseDir, manifest, scriptText, this.logger(), (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;
|
||||
}
|
||||
|
||||
@@ -282,7 +274,7 @@ export default class PluginService extends BaseService {
|
||||
|
||||
for (const pluginPath of pluginPaths) {
|
||||
if (pluginPath.indexOf('_') === 0) {
|
||||
logger.info(`Plugin name starts with "_" and has not been loaded: ${pluginPath}`);
|
||||
this.logger().info(`PluginService: Plugin name starts with "_" and has not been loaded: ${pluginPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -297,7 +289,7 @@ export default class PluginService extends BaseService {
|
||||
this.setPluginAt(plugin.id, plugin);
|
||||
|
||||
if (!this.pluginEnabled(settings, plugin.id)) {
|
||||
logger.info(`Not running disabled plugin: "${plugin.id}"`);
|
||||
this.logger().info(`PluginService: Not running disabled plugin: "${plugin.id}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -305,14 +297,14 @@ export default class PluginService extends BaseService {
|
||||
|
||||
await this.runPlugin(plugin);
|
||||
} catch (error) {
|
||||
logger.error(`Could not load plugin: ${pluginPath}`, error);
|
||||
this.logger().error(`PluginService: Could not load plugin: ${pluginPath}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async runPlugin(plugin: Plugin) {
|
||||
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_}.`);
|
||||
throw new Error(`PluginService: 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',
|
||||
@@ -324,22 +316,15 @@ export default class PluginService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
const pluginApi = new Global(this.platformImplementation_, plugin, this.store_);
|
||||
const pluginApi = new Global(this.logger(), this.platformImplementation_, plugin, this.store_);
|
||||
return this.runner_.run(plugin, pluginApi);
|
||||
}
|
||||
|
||||
public async installPlugin(jplPath: string): Promise<Plugin> {
|
||||
logger.info(`Installing plugin: "${jplPath}"`);
|
||||
this.logger().info(`PluginService: Installing plugin: "${jplPath}"`);
|
||||
|
||||
// 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);
|
||||
|
||||
const destPath = `${Setting.value('pluginDir')}/${preloadedPlugin.id}.jpl`;
|
||||
const destPath = `${Setting.value('pluginDir')}/${basename(jplPath)}`;
|
||||
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;
|
||||
@@ -358,12 +343,12 @@ export default class PluginService extends BaseService {
|
||||
}
|
||||
|
||||
public async uninstallPlugin(pluginId: string) {
|
||||
logger.info(`Uninstalling plugin: "${pluginId}"`);
|
||||
this.logger().info(`PluginService: 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}`);
|
||||
this.logger().error(`PluginService: Could not find plugin path to uninstall - nothing will be done: ${pluginId}`);
|
||||
} else {
|
||||
await shim.fsDriver().remove(path);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Plugin from '../Plugin';
|
||||
import Joplin from './Joplin';
|
||||
import Logger from '../../../Logger';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
@@ -15,8 +16,8 @@ export default class Global {
|
||||
private requireWhiteList_: string[] = null;
|
||||
// private consoleWrapper_:any = null;
|
||||
|
||||
constructor(implementation: any, plugin: Plugin, store: any) {
|
||||
this.joplin_ = new Joplin(implementation.joplin, plugin, store);
|
||||
constructor(logger: Logger, implementation: any, plugin: Plugin, store: any) {
|
||||
this.joplin_ = new Joplin(logger, implementation.joplin, plugin, store);
|
||||
// this.consoleWrapper_ = this.createConsoleWrapper(plugin.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.
|
||||
@@ -34,9 +35,9 @@ export default class Joplin {
|
||||
private interop_: JoplinInterop = null;
|
||||
private settings_: JoplinSettings = null;
|
||||
|
||||
constructor(implementation: any, plugin: Plugin, store: any) {
|
||||
constructor(logger: Logger, implementation: any, plugin: Plugin, store: any) {
|
||||
this.data_ = new JoplinData();
|
||||
this.plugins_ = new JoplinPlugins(plugin);
|
||||
this.plugins_ = new JoplinPlugins(logger, plugin);
|
||||
this.workspace_ = new JoplinWorkspace(implementation.workspace, store);
|
||||
this.filters_ = new JoplinFilters();
|
||||
this.commands_ = new JoplinCommands();
|
||||
|
||||
@@ -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(plugin: Plugin) {
|
||||
public constructor(logger: Logger, plugin: Plugin) {
|
||||
this.logger = logger;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class JoplinPlugins {
|
||||
if (script.onStart) {
|
||||
const startTime = Date.now();
|
||||
|
||||
logger.info(`Starting plugin: ${this.plugin.id}`);
|
||||
this.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;
|
||||
logger.error(`Uncaught exception in plugin "${this.plugin.id}":`, newError);
|
||||
this.logger.error(`Uncaught exception in plugin "${this.plugin.id}":`, newError);
|
||||
}).then(() => {
|
||||
logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
|
||||
this.logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
|
||||
this.plugin.emit('started');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ const gunzipFile = function(source, destination) {
|
||||
});
|
||||
};
|
||||
|
||||
function shimInit(sharp = null, keytar = null, React = null) {
|
||||
function shimInit(sharp = null, keytar = null) {
|
||||
keytar = (shim.isWindows() || shim.isMac()) && !shim.isPortable() ? keytar : null;
|
||||
|
||||
shim.fsDriver = () => {
|
||||
@@ -67,12 +67,6 @@ function shimInit(sharp = null, keytar = null, React = null) {
|
||||
return shim.fsDriver_;
|
||||
};
|
||||
|
||||
if (React) {
|
||||
shim.react = () => {
|
||||
return React;
|
||||
};
|
||||
}
|
||||
|
||||
shim.randomBytes = async count => {
|
||||
const buffer = require('crypto').randomBytes(count);
|
||||
return Array.from(buffer);
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
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 = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import htmlUtils from './htmlUtils';
|
||||
import linkReplacement from './MdToHtml/linkReplacement';
|
||||
import utils from './utils';
|
||||
const htmlUtils = require('./htmlUtils');
|
||||
const utils = require('./utils');
|
||||
// const noteStyle = require('./noteStyle').default;
|
||||
|
||||
// TODO: fix
|
||||
// const Setting = require('@joplin/lib/models/Setting').default;
|
||||
@@ -13,45 +13,9 @@ const md5 = require('md5');
|
||||
// relatively small.
|
||||
const inMemoryCache = new InMemoryCache(10);
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
class HtmlToHtml {
|
||||
constructor(options) {
|
||||
if (!options) options = {};
|
||||
this.resourceBaseUrl_ = 'resourceBaseUrl' in options ? options.resourceBaseUrl : null;
|
||||
this.ResourceModel_ = options.ResourceModel;
|
||||
this.cache_ = inMemoryCache;
|
||||
@@ -72,7 +36,7 @@ export default class HtmlToHtml {
|
||||
return this.fsDriver_;
|
||||
}
|
||||
|
||||
splitHtml(html: string) {
|
||||
splitHtml(html) {
|
||||
const trimmedHtml = html.trimStart();
|
||||
if (trimmedHtml.indexOf('<style>') !== 0) return { html: html, css: '' };
|
||||
|
||||
@@ -85,20 +49,17 @@ export default class HtmlToHtml {
|
||||
};
|
||||
}
|
||||
|
||||
async allAssets(/* theme*/): Promise<any[]> {
|
||||
async allAssets(/* theme*/) {
|
||||
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: string, _theme: any, options: RenderOptions): Promise<RenderResult> {
|
||||
options = {
|
||||
async render(markup, _theme, options) {
|
||||
options = Object.assign({}, {
|
||||
splitted: false,
|
||||
postMessageSyntax: 'postMessage',
|
||||
enableLongPress: false,
|
||||
...options,
|
||||
};
|
||||
}, options);
|
||||
|
||||
const cacheKey = md5(escape(markup));
|
||||
let html = this.cache_.value(cacheKey);
|
||||
@@ -106,7 +67,7 @@ export default class HtmlToHtml {
|
||||
if (!html) {
|
||||
html = htmlUtils.sanitizeHtml(markup);
|
||||
|
||||
html = htmlUtils.processImageTags(html, (data: any) => {
|
||||
html = htmlUtils.processImageTags(html, data => {
|
||||
if (!data.src) return null;
|
||||
|
||||
const r = utils.imageReplacement(this.ResourceModel_, data.src, options.resources, this.resourceBaseUrl_);
|
||||
@@ -124,24 +85,6 @@ export default 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);
|
||||
@@ -155,13 +98,13 @@ export default class HtmlToHtml {
|
||||
|
||||
// const lightTheme = themeStyle(Setting.THEME_LIGHT);
|
||||
// let cssStrings = noteStyle(lightTheme);
|
||||
let cssStrings: string[] = [];
|
||||
let cssStrings = [];
|
||||
|
||||
if (options.splitted) {
|
||||
const splitted = this.splitHtml(html);
|
||||
cssStrings = [splitted.css].concat(cssStrings);
|
||||
|
||||
const output: RenderResult = {
|
||||
const output = {
|
||||
html: splitted.html,
|
||||
pluginAssets: [],
|
||||
};
|
||||
@@ -181,3 +124,5 @@ export default class HtmlToHtml {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HtmlToHtml;
|
||||
@@ -1,6 +1,6 @@
|
||||
import MdToHtml from './MdToHtml';
|
||||
import HtmlToHtml from './HtmlToHtml';
|
||||
import htmlUtils from './htmlUtils';
|
||||
const HtmlToHtml = require('./HtmlToHtml');
|
||||
const htmlUtils = require('./htmlUtils');
|
||||
const MarkdownIt = require('markdown-it');
|
||||
|
||||
export enum MarkupLanguage {
|
||||
|
||||
@@ -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,6 +138,10 @@ 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.
|
||||
@@ -378,7 +382,7 @@ export default class MdToHtml {
|
||||
const markdownIt = new MarkdownIt({
|
||||
breaks: !this.pluginEnabled('softbreaks'),
|
||||
typographer: this.pluginEnabled('typographer'),
|
||||
linkify: true,
|
||||
// linkify: true,
|
||||
html: true,
|
||||
highlight: (str: string, lang: string) => {
|
||||
let outputCodeHtml = '';
|
||||
@@ -409,16 +413,10 @@ export default class MdToHtml {
|
||||
outputCodeHtml = markdownIt.utils.escapeHtml(trimmedStr);
|
||||
}
|
||||
|
||||
const html = `<div class="joplin-editable">${sourceBlockHtml}<pre class="hljs"><code>${outputCodeHtml}</code></pre></div>`;
|
||||
|
||||
if (rules.fence) {
|
||||
return {
|
||||
wrapCode: false,
|
||||
html: html,
|
||||
};
|
||||
} else {
|
||||
return html;
|
||||
}
|
||||
return {
|
||||
wrapCode: false,
|
||||
html: `<div class="joplin-editable">${sourceBlockHtml}<pre class="hljs"><code>${outputCodeHtml}</code></pre></div>`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
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}'`;
|
||||
|
||||
const 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}`;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RuleOptions } from '../../MdToHtml';
|
||||
import htmlUtils from '../../htmlUtils';
|
||||
import utils from '../../utils';
|
||||
|
||||
const htmlUtils = require('../../htmlUtils.js');
|
||||
const utils = require('../../utils');
|
||||
|
||||
function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) {
|
||||
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RuleOptions } from '../../MdToHtml';
|
||||
import htmlUtils from '../../htmlUtils';
|
||||
import utils from '../../utils';
|
||||
|
||||
const utils = require('../../utils');
|
||||
const htmlUtils = require('../../htmlUtils.js');
|
||||
|
||||
function plugin(markdownIt: any, ruleOptions: RuleOptions) {
|
||||
const defaultRender = markdownIt.renderer.rules.image;
|
||||
@@ -22,7 +23,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};}, ${utils.longPressDelay});`;
|
||||
const touchStart = `t=setTimeout(()=>{t=null; ${longPressHandler};}, ${ruleOptions.longPressDelay});`;
|
||||
const cancel = 'if (!!t) clearTimeout(t); t=null';
|
||||
|
||||
js = ` ontouchstart="${touchStart}" ontouchend="${cancel}" ontouchcancel="${cancel}" ontouchmove="${cancel}"`;
|
||||
|
||||
@@ -1,26 +1,95 @@
|
||||
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];
|
||||
const href = utils.getAttr(token.attrs, 'href');
|
||||
let href = utils.getAttr(token.attrs, 'href');
|
||||
const resourceHrefInfo = urlUtils.parseResourceUrl(href);
|
||||
const isResourceUrl = ruleOptions.resources && !!resourceHrefInfo;
|
||||
const title = utils.getAttr(token.attrs, 'title', isResourceUrl ? '' : href);
|
||||
let title = utils.getAttr(token.attrs, 'title', isResourceUrl ? '' : href);
|
||||
|
||||
return linkReplacement(href, {
|
||||
title,
|
||||
resources: ruleOptions.resources,
|
||||
ResourceModel: ruleOptions.ResourceModel,
|
||||
linkRenderingType: ruleOptions.linkRenderingType,
|
||||
plainResourceRendering: ruleOptions.plainResourceRendering,
|
||||
postMessageSyntax: ruleOptions.postMessageSyntax,
|
||||
enableLongPress: ruleOptions.enableLongPress,
|
||||
});
|
||||
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}`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,8 +6,6 @@ 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',
|
||||
@@ -32,7 +30,7 @@ const selfClosingElements = [
|
||||
|
||||
class HtmlUtils {
|
||||
|
||||
attributesHtml(attr: any) {
|
||||
attributesHtml(attr) {
|
||||
const output = [];
|
||||
|
||||
for (const n in attr) {
|
||||
@@ -43,10 +41,10 @@ class HtmlUtils {
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
processImageTags(html: string, callback: Function) {
|
||||
processImageTags(html, callback) {
|
||||
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}>`;
|
||||
@@ -68,40 +66,15 @@ class HtmlUtils {
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
isSelfClosingTag(tagName) {
|
||||
return selfClosingElements.includes(tagName.toLowerCase());
|
||||
}
|
||||
|
||||
// TODO: copied from @joplin/lib
|
||||
stripHtml(html: string) {
|
||||
const output: string[] = [];
|
||||
stripHtml(html) {
|
||||
const output = [];
|
||||
|
||||
const tagStack: string[] = [];
|
||||
const tagStack = [];
|
||||
|
||||
const currentTag = () => {
|
||||
if (!tagStack.length) return '';
|
||||
@@ -112,16 +85,16 @@ class HtmlUtils {
|
||||
|
||||
const parser = new htmlparser2.Parser({
|
||||
|
||||
onopentag: (name: string) => {
|
||||
onopentag: (name) => {
|
||||
tagStack.push(name.toLowerCase());
|
||||
},
|
||||
|
||||
ontext: (decodedText: string) => {
|
||||
ontext: (decodedText) => {
|
||||
if (disallowedTags.includes(currentTag())) return;
|
||||
output.push(decodedText);
|
||||
},
|
||||
|
||||
onclosetag: (name: string) => {
|
||||
onclosetag: (name) => {
|
||||
if (currentTag() === name.toLowerCase()) tagStack.pop();
|
||||
},
|
||||
|
||||
@@ -133,16 +106,16 @@ class HtmlUtils {
|
||||
return output.join('').replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
sanitizeHtml(html: string, options: any = null) {
|
||||
sanitizeHtml(html, options = 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: string[] = [];
|
||||
const output = [];
|
||||
|
||||
const tagStack: string[] = [];
|
||||
const tagStack = [];
|
||||
|
||||
const currentTag = () => {
|
||||
if (!tagStack.length) return '';
|
||||
@@ -162,7 +135,7 @@ class HtmlUtils {
|
||||
|
||||
const parser = new htmlparser2.Parser({
|
||||
|
||||
onopentag: (name: string, attrs: any) => {
|
||||
onopentag: (name, attrs) => {
|
||||
tagStack.push(name.toLowerCase());
|
||||
|
||||
if (disallowedTags.includes(currentTag())) return;
|
||||
@@ -198,7 +171,7 @@ class HtmlUtils {
|
||||
output.push(`<${name}${attrHtml}${closingSign}`);
|
||||
},
|
||||
|
||||
ontext: (decodedText: string) => {
|
||||
ontext: (decodedText) => {
|
||||
if (disallowedTags.includes(currentTag())) return;
|
||||
|
||||
if (currentTag() === 'style') {
|
||||
@@ -211,7 +184,7 @@ class HtmlUtils {
|
||||
}
|
||||
},
|
||||
|
||||
onclosetag: (name: string) => {
|
||||
onclosetag: (name) => {
|
||||
const current = currentTag();
|
||||
|
||||
if (current === name.toLowerCase()) tagStack.pop();
|
||||
@@ -233,4 +206,6 @@ class HtmlUtils {
|
||||
|
||||
}
|
||||
|
||||
export default new HtmlUtils();
|
||||
const htmlUtils = new HtmlUtils();
|
||||
|
||||
module.exports = htmlUtils;
|
||||
@@ -1,9 +1,9 @@
|
||||
import MarkupToHtml, { MarkupLanguage } from './MarkupToHtml';
|
||||
import MdToHtml from './MdToHtml';
|
||||
import HtmlToHtml from './HtmlToHtml';
|
||||
import utils from './utils';
|
||||
const HtmlToHtml = require('./HtmlToHtml');
|
||||
const setupLinkify = require('./MdToHtml/setupLinkify');
|
||||
const assetsToHeaders = require('./assetsToHeaders');
|
||||
const utils = require('./utils');
|
||||
|
||||
export {
|
||||
MarkupToHtml,
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
// 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,
|
||||
};
|
||||
5746
packages/renderer/package-lock.json
generated
5746
packages/renderer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,20 +12,17 @@
|
||||
"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",
|
||||
"test": "jest",
|
||||
"test-ci": "test"
|
||||
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.6",
|
||||
"jest": "^26.6.3",
|
||||
"jasmine": "^3.5.0",
|
||||
"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",
|
||||
|
||||
@@ -9,9 +9,9 @@ const FetchStatuses = {
|
||||
FETCH_STATUS_ERROR: 3,
|
||||
};
|
||||
|
||||
const utils: any = {};
|
||||
const utils = {};
|
||||
|
||||
utils.getAttr = function(attrs: string[], name: string, defaultValue: string = null) {
|
||||
utils.getAttr = function(attrs, name, defaultValue = 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: string) {
|
||||
utils.resourceStatusImage = function(status) {
|
||||
if (status === 'notDownloaded') return utils.notDownloadedResource();
|
||||
return utils.resourceStatusFile(status);
|
||||
};
|
||||
|
||||
utils.resourceStatusFile = function(status: string) {
|
||||
utils.resourceStatusFile = function(status) {
|
||||
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: string) {
|
||||
throw new Error(`Unknown status: ${status}`);
|
||||
};
|
||||
|
||||
utils.resourceStatusIndex = function(status: string) {
|
||||
utils.resourceStatusIndex = function(status) {
|
||||
if (status === 'error') return -1;
|
||||
if (status === 'notDownloaded') return 0;
|
||||
if (status === 'downloading') return 1;
|
||||
@@ -87,7 +87,7 @@ utils.resourceStatusIndex = function(status: string) {
|
||||
throw new Error(`Unknown status: ${status}`);
|
||||
};
|
||||
|
||||
utils.resourceStatusName = function(index: number) {
|
||||
utils.resourceStatusName = function(index) {
|
||||
if (index === -1) return 'error';
|
||||
if (index === 0) return 'notDownloaded';
|
||||
if (index === 1) return 'downloading';
|
||||
@@ -97,7 +97,7 @@ utils.resourceStatusName = function(index: number) {
|
||||
throw new Error(`Unknown index: ${index}`);
|
||||
};
|
||||
|
||||
utils.resourceStatus = function(ResourceModel: any, resourceInfo: any) {
|
||||
utils.resourceStatus = function(ResourceModel, resourceInfo) {
|
||||
if (!ResourceModel) return 'ready';
|
||||
|
||||
let resourceStatus = 'ready';
|
||||
@@ -122,7 +122,7 @@ utils.resourceStatus = function(ResourceModel: any, resourceInfo: any) {
|
||||
return resourceStatus;
|
||||
};
|
||||
|
||||
utils.imageReplacement = function(ResourceModel: any, src: string, resources: any, resourceBaseUrl: string) {
|
||||
utils.imageReplacement = function(ResourceModel, src, resources, resourceBaseUrl) {
|
||||
if (!ResourceModel || !resources) return null;
|
||||
|
||||
if (!ResourceModel.isResourceUrl(src)) return null;
|
||||
@@ -151,8 +151,4 @@ utils.imageReplacement = function(ResourceModel: any, src: string, resources: an
|
||||
return null;
|
||||
};
|
||||
|
||||
// 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;
|
||||
module.exports = utils;
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
// Dependencies:
|
||||
//
|
||||
// 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.
|
||||
// sudo apt install gettext
|
||||
// sudo apt install translate-toolkit
|
||||
|
||||
const rootDir = `${__dirname}/../..`;
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ 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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, at a low-level, the behaviour of the Markdown editor (CodeMirror)
|
||||
- Create an editor plugin to modify 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://joplinapp.org/api/references/plugin_api/classes/joplin.html) 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://github.com/laurent22/joplin/blob/dev/readme/api/get_started/plugins.md) for a detailed documentation about each supported feature.
|
||||
|
||||
Reference in New Issue
Block a user