1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-29 23:48:19 +02:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Laurent Cozic
28be0dbd36 update 2020-11-19 12:28:16 +00:00
Laurent Cozic
cd406cba51 update 2020-11-18 22:58:09 +00:00
Laurent Cozic
57c0305ad8 gui 2020-11-18 17:36:04 +00:00
83 changed files with 1402 additions and 8575 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

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

View File

@@ -30,12 +30,12 @@ module.exports = {
],
testPathIgnorePatterns: [
'<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'],
};

View File

@@ -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",

View File

@@ -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"

View 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);
}
}
}));
});

View File

@@ -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.',
'',
'![snapshot-DAE9FC15-88E3-46CF-B744-DA9B1B56EB57.jpg](:/3d0f4d01abc02cf8c4dc1c796df8c4b2)',
].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
});
});

View File

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

View File

@@ -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);
}
}));
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}));
});

View File

@@ -262,14 +262,14 @@ describe('models_Note', function() {
for (const testCase of testCases) {
const [useAbsolutePaths, input, expected] = testCase;
const internalToExternal = await Note.replaceResourceInternalToExternalLinks(input, { useAbsolutePaths });
expect(internalToExternal).toBe(expected);
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)');
}));
});

View File

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

View File

@@ -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);
}));
});

View File

@@ -3,7 +3,7 @@
const time = require('@joplin/lib/time').default;
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, 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');

View File

@@ -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);

View File

@@ -538,7 +538,7 @@ describe('services_rest_Api', function() {
const r3 = await api.route(RequestMethod.GET, 'folders', { ...baseQuery, page: 3 });
expect(r3.items.length).toBe(0);
expect(r3.has_more).toBe(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);
}
}));

View File

@@ -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',
}

View File

@@ -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);

View File

@@ -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 };

View File

@@ -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

View File

@@ -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'));

View File

@@ -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) {

View File

@@ -16,12 +16,12 @@ import { MenuItem, MenuItemLocation } from '@joplin/lib/services/plugins/api/typ
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import menuCommandNames from './menuCommandNames';
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
import bridge from '../services/bridge';
const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js');
const packageInfo = require('../packageInfo.js');
const { 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'),

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);
});
}

View File

@@ -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;

View File

@@ -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');

View File

@@ -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,
},
};

View File

@@ -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}`;

View File

@@ -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 {

View File

@@ -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=""
/>
);
}
}

View File

@@ -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))
);
}

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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"

View File

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

View File

@@ -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(

View File

@@ -46,13 +46,13 @@ const MigrationService = require('./services/MigrationService');
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');
const appLogger = Logger.create('App');
// const ntpClient = require('./vendor/ntp-client');
// ntpClient.dgram = require('dgram');
export default class BaseApplication {
private logger_: Logger;
private dbLogger_: Logger;
private eventEmitter_: any;
private scheduleAutoAddResourcesIID_: any = null;
private database_: any = null;
@@ -68,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();

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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++) {

View File

@@ -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);
});

View File

@@ -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) {

View File

@@ -276,7 +276,7 @@ class Note extends BaseItem {
includeTimestamps: true,
}, options);
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language', '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');

View File

@@ -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'),

View File

@@ -3,7 +3,7 @@ import { Pagination, PaginationOrder } from './types';
export interface ModelFeedPage {
items: any[];
has_more: boolean;
has_more?: boolean;
total?: number;
}
@@ -52,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;
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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]);

View File

@@ -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);

View File

@@ -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}.`);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -2,16 +2,16 @@ import Plugin from '../Plugin';
import Logger from '../../../Logger';
import { ContentScriptType, Script } from './types';
const logger = Logger.create('joplin.plugins');
/**
* This class provides access to plugin-related features.
*/
export default class JoplinPlugins {
private logger: Logger;
private plugin: Plugin;
public constructor(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');
});
}

View File

@@ -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);

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -68,9 +68,9 @@ function slugify(s: string): string {
const inMemoryCache = new InMemoryCache(20);
export interface ExtraRendererRule {
id: string;
module: any;
assetPath: string;
id: string,
module: any,
assetPath: string,
}
export interface Options {
@@ -138,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>`,
};
},
});

View File

@@ -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);
});
});

View File

@@ -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}`;
}

View File

@@ -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);

View File

@@ -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}"`;

View File

@@ -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}`;
};
}

View File

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

View File

@@ -6,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;

View File

@@ -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,

View File

@@ -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,
};

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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;

View File

@@ -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}/../..`;

View File

@@ -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}`);
}

View File

@@ -26,8 +26,8 @@ With plugins you can directly modify Joplin by adding new features to the applic
- Create a module to export or import data into Joplin
- Define new settings and setting sections, and get/set them from the plugin
- Create a new Markdown plugin to render custom markup.
- Create an editor plugin to modify, 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.