1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-02-28 09:22:25 +02:00

Compare commits

...

6 Commits

Author SHA1 Message Date
Laurent Cozic
9bba9a8ed6 Plugins: Fixed issue with toolbar button key not being unique 2020-11-20 16:19:57 +00:00
Laurent Cozic
3344e84c7c fix link 2020-11-20 15:52:08 +00:00
Laurent Cozic
e0c2b62a6c Tools: Fixed tests when running them from root 2020-11-20 14:04:02 +00:00
Laurent Cozic
544d879c0b Plugins: Use plugin ID as filename 2020-11-19 23:46:04 +00:00
Laurent Cozic
28f75449d7 Desktop release v1.4.11 2020-11-19 21:02:03 +00:00
Laurent Cozic
fdc84aa6bb Desktop: Upgrade to Electron 10 2020-11-19 21:01:19 +00:00
35 changed files with 7957 additions and 954 deletions

View File

@@ -1351,6 +1351,9 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/renderer/HtmlToHtml.d.ts
packages/renderer/HtmlToHtml.js
packages/renderer/HtmlToHtml.js.map
packages/renderer/InMemoryCache.d.ts
packages/renderer/InMemoryCache.js
packages/renderer/InMemoryCache.js.map
@@ -1360,6 +1363,12 @@ packages/renderer/MarkupToHtml.js.map
packages/renderer/MdToHtml.d.ts
packages/renderer/MdToHtml.js
packages/renderer/MdToHtml.js.map
packages/renderer/MdToHtml/linkReplacement.d.ts
packages/renderer/MdToHtml/linkReplacement.js
packages/renderer/MdToHtml/linkReplacement.js.map
packages/renderer/MdToHtml/linkReplacement.test.d.ts
packages/renderer/MdToHtml/linkReplacement.test.js
packages/renderer/MdToHtml/linkReplacement.test.js.map
packages/renderer/MdToHtml/rules/checkbox.d.ts
packages/renderer/MdToHtml/rules/checkbox.js
packages/renderer/MdToHtml/rules/checkbox.js.map
@@ -1393,6 +1402,9 @@ packages/renderer/MdToHtml/rules/mermaid.js.map
packages/renderer/MdToHtml/rules/sanitize_html.d.ts
packages/renderer/MdToHtml/rules/sanitize_html.js
packages/renderer/MdToHtml/rules/sanitize_html.js.map
packages/renderer/htmlUtils.d.ts
packages/renderer/htmlUtils.js
packages/renderer/htmlUtils.js.map
packages/renderer/index.d.ts
packages/renderer/index.js
packages/renderer/index.js.map
@@ -1402,4 +1414,7 @@ packages/renderer/noteStyle.js.map
packages/renderer/pathUtils.d.ts
packages/renderer/pathUtils.js
packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

15
.gitignore vendored
View File

@@ -1343,6 +1343,9 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/renderer/HtmlToHtml.d.ts
packages/renderer/HtmlToHtml.js
packages/renderer/HtmlToHtml.js.map
packages/renderer/InMemoryCache.d.ts
packages/renderer/InMemoryCache.js
packages/renderer/InMemoryCache.js.map
@@ -1352,6 +1355,12 @@ packages/renderer/MarkupToHtml.js.map
packages/renderer/MdToHtml.d.ts
packages/renderer/MdToHtml.js
packages/renderer/MdToHtml.js.map
packages/renderer/MdToHtml/linkReplacement.d.ts
packages/renderer/MdToHtml/linkReplacement.js
packages/renderer/MdToHtml/linkReplacement.js.map
packages/renderer/MdToHtml/linkReplacement.test.d.ts
packages/renderer/MdToHtml/linkReplacement.test.js
packages/renderer/MdToHtml/linkReplacement.test.js.map
packages/renderer/MdToHtml/rules/checkbox.d.ts
packages/renderer/MdToHtml/rules/checkbox.js
packages/renderer/MdToHtml/rules/checkbox.js.map
@@ -1385,6 +1394,9 @@ packages/renderer/MdToHtml/rules/mermaid.js.map
packages/renderer/MdToHtml/rules/sanitize_html.d.ts
packages/renderer/MdToHtml/rules/sanitize_html.js
packages/renderer/MdToHtml/rules/sanitize_html.js.map
packages/renderer/htmlUtils.d.ts
packages/renderer/htmlUtils.js
packages/renderer/htmlUtils.js.map
packages/renderer/index.d.ts
packages/renderer/index.js
packages/renderer/index.js.map
@@ -1394,4 +1406,7 @@ packages/renderer/noteStyle.js.map
packages/renderer/pathUtils.d.ts
packages/renderer/pathUtils.js
packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

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

View File

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

View File

@@ -3,7 +3,9 @@ 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');
@@ -260,4 +262,23 @@ 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

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

View File

@@ -216,6 +216,7 @@ async function switchClient(id, options = null) {
await Setting.reset();
Setting.setConstant('resourceDirName', resourceDirName(id));
Setting.setConstant('resourceDir', resourceDir(id));
Setting.setConstant('pluginDir', pluginDir(id));
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
@@ -294,6 +295,11 @@ function resourceDir(id = null) {
return `${__dirname}/data/${resourceDirName(id)}`;
}
function pluginDir(id = null) {
if (id === null) id = currentClient_;
return `${__dirname}/data/plugins-${id}`;
}
async function setupDatabaseAndSynchronizer(id = null, options = null) {
if (id === null) id = currentClient_;
@@ -307,6 +313,9 @@ async function setupDatabaseAndSynchronizer(id = null, options = null) {
await fs.remove(resourceDir(id));
await fs.mkdirp(resourceDir(id), 0o755);
await fs.remove(pluginDir(id));
await fs.mkdirp(pluginDir(id), 0o755);
if (!synchronizers_[id]) {
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
const syncTarget = new SyncTargetClass(db(id));
@@ -727,7 +736,7 @@ class TestApp extends BaseApplication {
}
async profileDir() {
return await Setting.value('profileDir');
return Setting.value('profileDir');
}
async destroy() {

View File

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

View File

@@ -170,8 +170,8 @@ export class Bridge {
return require('electron').shell.openExternal(url);
}
openItem(fullPath: string) {
return require('electron').shell.openItem(fullPath);
async openItem(fullPath: string) {
return require('electron').shell.openPath(fullPath);
}
checkForUpdates(inBackground: boolean, window: any, logFilePath: string, options: any) {

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.4.10",
"version": "1.4.11",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -11,7 +11,8 @@
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json",
"start": "gulp build && electron . --env dev --log-level debug --no-welcome --open-dev-tools",
"test": "jest"
"test": "jest",
"test-ci": "test"
},
"repository": {
"type": "git",
@@ -97,9 +98,9 @@
"app-builder-bin": "^1.9.11",
"babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1",
"electron": "^8.2.5",
"electron-builder": "22.3.2",
"electron-rebuild": "^1.10.1",
"electron": "^10.1.6",
"electron-builder": "22.9.1",
"electron-rebuild": "^2.3.2",
"glob": "^7.1.6",
"gulp": "^4.0.2",
"jest": "^26.6.3",

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import Global from './api/Global';
import BasePluginRunner from './BasePluginRunner';
import BaseService from '../BaseService';
import shim from '../../shim';
import { filename, dirname, rtrimSlashes, basename } from '../../path-utils';
import { filename, dirname, rtrimSlashes } from '../../path-utils';
import Setting from '../../models/Setting';
import Logger from '../../Logger';
const compareVersions = require('compare-versions');
@@ -251,6 +251,11 @@ export default class PluginService extends BaseService {
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;
}
@@ -326,8 +331,15 @@ export default class PluginService extends BaseService {
public async installPlugin(jplPath: string): Promise<Plugin> {
logger.info(`Installing plugin: "${jplPath}"`);
const destPath = `${Setting.value('pluginDir')}/${basename(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`;
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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