1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
This commit is contained in:
Laurent Cozic 2020-11-23 11:52:36 +00:00
commit 86bace70a5
24 changed files with 6395 additions and 152 deletions

View File

@ -1351,6 +1351,9 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js packages/lib/versionInfo.js
packages/lib/versionInfo.js.map 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.d.ts
packages/renderer/InMemoryCache.js packages/renderer/InMemoryCache.js
packages/renderer/InMemoryCache.js.map packages/renderer/InMemoryCache.js.map
@ -1360,6 +1363,12 @@ packages/renderer/MarkupToHtml.js.map
packages/renderer/MdToHtml.d.ts packages/renderer/MdToHtml.d.ts
packages/renderer/MdToHtml.js packages/renderer/MdToHtml.js
packages/renderer/MdToHtml.js.map 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.d.ts
packages/renderer/MdToHtml/rules/checkbox.js packages/renderer/MdToHtml/rules/checkbox.js
packages/renderer/MdToHtml/rules/checkbox.js.map 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.d.ts
packages/renderer/MdToHtml/rules/sanitize_html.js packages/renderer/MdToHtml/rules/sanitize_html.js
packages/renderer/MdToHtml/rules/sanitize_html.js.map 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.d.ts
packages/renderer/index.js packages/renderer/index.js
packages/renderer/index.js.map packages/renderer/index.js.map
@ -1402,4 +1414,7 @@ packages/renderer/noteStyle.js.map
packages/renderer/pathUtils.d.ts packages/renderer/pathUtils.d.ts
packages/renderer/pathUtils.js packages/renderer/pathUtils.js
packages/renderer/pathUtils.js.map packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD # 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.d.ts
packages/lib/versionInfo.js packages/lib/versionInfo.js
packages/lib/versionInfo.js.map 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.d.ts
packages/renderer/InMemoryCache.js packages/renderer/InMemoryCache.js
packages/renderer/InMemoryCache.js.map packages/renderer/InMemoryCache.js.map
@ -1352,6 +1355,12 @@ packages/renderer/MarkupToHtml.js.map
packages/renderer/MdToHtml.d.ts packages/renderer/MdToHtml.d.ts
packages/renderer/MdToHtml.js packages/renderer/MdToHtml.js
packages/renderer/MdToHtml.js.map 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.d.ts
packages/renderer/MdToHtml/rules/checkbox.js packages/renderer/MdToHtml/rules/checkbox.js
packages/renderer/MdToHtml/rules/checkbox.js.map 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.d.ts
packages/renderer/MdToHtml/rules/sanitize_html.js packages/renderer/MdToHtml/rules/sanitize_html.js
packages/renderer/MdToHtml/rules/sanitize_html.js.map 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.d.ts
packages/renderer/index.js packages/renderer/index.js
packages/renderer/index.js.map packages/renderer/index.js.map
@ -1394,4 +1406,7 @@ packages/renderer/noteStyle.js.map
packages/renderer/pathUtils.d.ts packages/renderer/pathUtils.d.ts
packages/renderer/pathUtils.js packages/renderer/pathUtils.js
packages/renderer/pathUtils.js.map packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

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

View File

@ -208,6 +208,13 @@ export enum MenuItemLocation {
* @deprecated Do not use - same as NoteListContextMenu * @deprecated Do not use - same as NoteListContextMenu
*/ */
Context = 'context', 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', NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu', EditorContextMenu = 'editorContextMenu',
} }

View File

@ -21,6 +21,20 @@ 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 // Commands that return a result and take argument can only be used
// programmatically, so it's not necessary to set a label and icon. // programmatically, so it's not necessary to set a label and icon.
await joplin.commands.register({ await joplin.commands.register({
@ -40,6 +54,8 @@ joplin.plugins.register({
await joplin.views.menuItems.create('myMenuItem1', 'testCommand1', MenuItemLocation.Tools, { accelerator: 'CmdOrCtrl+Alt+Shift+B' }); 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('myMenuItem2', 'testCommand2', MenuItemLocation.Tools);
await joplin.views.menuItems.create('contextMenuItem1', 'contextMenuCommandExample', MenuItemLocation.NoteListContextMenu);
console.info('Running command with arguments...'); console.info('Running command with arguments...');
const result = await joplin.commands.execute('commandWithResult', 'abcd', 123); const result = await joplin.commands.execute('commandWithResult', 'abcd', 123);
console.info('Result was: ' + result); console.info('Result was: ' + result);

View File

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

View File

@ -186,7 +186,7 @@ export default class NoteListUtils {
if (location !== MenuItemLocation.Context && location !== MenuItemLocation.NoteListContextMenu) continue; if (location !== MenuItemLocation.Context && location !== MenuItemLocation.NoteListContextMenu) continue;
menu.append( menu.append(
new MenuItem(menuUtils.commandToStatefulMenuItem(info.view.commandName)) new MenuItem(menuUtils.commandToStatefulMenuItem(info.view.commandName, noteIds))
); );
} }

View File

@ -11,7 +11,8 @@
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json", "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",
"start": "gulp build && electron . --env dev --log-level debug --no-welcome --open-dev-tools", "start": "gulp build && electron . --env dev --log-level debug --no-welcome --open-dev-tools",
"test": "jest" "test": "jest",
"test-ci": "test"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import MdToHtml from './MdToHtml'; import MdToHtml from './MdToHtml';
const HtmlToHtml = require('./HtmlToHtml'); import HtmlToHtml from './HtmlToHtml';
const htmlUtils = require('./htmlUtils'); import htmlUtils from './htmlUtils';
const MarkdownIt = require('markdown-it'); const MarkdownIt = require('markdown-it');
export enum MarkupLanguage { 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` // to display a context menu. Used in `image.ts` and `link_open.ts`
enableLongPress?: boolean; 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. // Use by `link_open` rule.
// linkRenderingType = 1 is the regular rendering and clicking on it is handled via embedded JS (in onclick attribute) // 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. // 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}'`;
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,7 +1,6 @@
import { RuleOptions } from '../../MdToHtml'; import { RuleOptions } from '../../MdToHtml';
import htmlUtils from '../../htmlUtils';
const htmlUtils = require('../../htmlUtils.js'); import utils from '../../utils';
const utils = require('../../utils');
function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) { function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) {
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl); const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl);

View File

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

View File

@ -1,95 +1,26 @@
import { RuleOptions } from '../../MdToHtml'; 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 urlUtils = require('../../urlUtils.js');
const { getClassNameForMimeType } = require('font-awesome-filetypes');
function plugin(markdownIt: any, ruleOptions: RuleOptions) { function plugin(markdownIt: any, ruleOptions: RuleOptions) {
markdownIt.renderer.rules.link_open = function(tokens: any[], idx: number) { markdownIt.renderer.rules.link_open = function(tokens: any[], idx: number) {
const token = tokens[idx]; const token = tokens[idx];
let href = utils.getAttr(token.attrs, 'href'); const href = utils.getAttr(token.attrs, 'href');
const resourceHrefInfo = urlUtils.parseResourceUrl(href); const resourceHrefInfo = urlUtils.parseResourceUrl(href);
const isResourceUrl = ruleOptions.resources && !!resourceHrefInfo; 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 = ''; return linkReplacement(href, {
let icon = ''; title,
let hrefAttr = '#'; resources: ruleOptions.resources,
let mime = ''; ResourceModel: ruleOptions.ResourceModel,
let resourceId = ''; linkRenderingType: ruleOptions.linkRenderingType,
if (isResourceUrl) { plainResourceRendering: ruleOptions.plainResourceRendering,
resourceId = resourceHrefInfo.itemId; postMessageSyntax: ruleOptions.postMessageSyntax,
enableLongPress: ruleOptions.enableLongPress,
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 { RuleOptions } from '../../MdToHtml';
import htmlUtils from '../../htmlUtils';
const md5 = require('md5'); const md5 = require('md5');
const htmlUtils = require('../../htmlUtils');
export default { export default {
plugin: function(markdownIt: any, ruleOptions: RuleOptions) { plugin: function(markdownIt: any, ruleOptions: RuleOptions) {

View File

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

View File

@ -1,9 +1,9 @@
import MarkupToHtml, { MarkupLanguage } from './MarkupToHtml'; import MarkupToHtml, { MarkupLanguage } from './MarkupToHtml';
import MdToHtml from './MdToHtml'; import MdToHtml from './MdToHtml';
const HtmlToHtml = require('./HtmlToHtml'); import HtmlToHtml from './HtmlToHtml';
import utils from './utils';
const setupLinkify = require('./MdToHtml/setupLinkify'); const setupLinkify = require('./MdToHtml/setupLinkify');
const assetsToHeaders = require('./assetsToHeaders'); const assetsToHeaders = require('./assetsToHeaders');
const utils = require('./utils');
export { export {
MarkupToHtml, 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", "buildAssets": "node Tools/buildAssets.js",
"prepublishOnly": "npm run buildAssets", "prepublishOnly": "npm run buildAssets",
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json", "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": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.14.6", "@types/node": "^14.14.6",
"jest": "^26.6.3",
"typescript": "^4.0.5" "typescript": "^4.0.5"
}, },
"dependencies": { "dependencies": {
"@joplin/fork-htmlparser2": "^4.1.8", "@joplin/fork-htmlparser2": "^4.1.8",
"@types/jest": "^26.0.15",
"font-awesome-filetypes": "^2.1.0", "font-awesome-filetypes": "^2.1.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"highlight.js": "^10.2.1", "highlight.js": "^10.2.1",

View File

@ -9,9 +9,9 @@ const FetchStatuses = {
FETCH_STATUS_ERROR: 3, 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++) { for (let i = 0; i < attrs.length; i++) {
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null; 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(); if (status === 'notDownloaded') return utils.notDownloadedResource();
return utils.resourceStatusFile(status); return utils.resourceStatusFile(status);
}; };
utils.resourceStatusFile = function(status) { utils.resourceStatusFile = function(status: string) {
if (status === 'notDownloaded') return utils.notDownloadedResource(); if (status === 'notDownloaded') return utils.notDownloadedResource();
if (status === 'downloading') return utils.loaderImage(); if (status === 'downloading') return utils.loaderImage();
if (status === 'encrypted') return utils.loaderImage(); if (status === 'encrypted') return utils.loaderImage();
@ -77,7 +77,7 @@ utils.resourceStatusFile = function(status) {
throw new Error(`Unknown status: ${status}`); throw new Error(`Unknown status: ${status}`);
}; };
utils.resourceStatusIndex = function(status) { utils.resourceStatusIndex = function(status: string) {
if (status === 'error') return -1; if (status === 'error') return -1;
if (status === 'notDownloaded') return 0; if (status === 'notDownloaded') return 0;
if (status === 'downloading') return 1; if (status === 'downloading') return 1;
@ -87,7 +87,7 @@ utils.resourceStatusIndex = function(status) {
throw new Error(`Unknown status: ${status}`); throw new Error(`Unknown status: ${status}`);
}; };
utils.resourceStatusName = function(index) { utils.resourceStatusName = function(index: number) {
if (index === -1) return 'error'; if (index === -1) return 'error';
if (index === 0) return 'notDownloaded'; if (index === 0) return 'notDownloaded';
if (index === 1) return 'downloading'; if (index === 1) return 'downloading';
@ -97,7 +97,7 @@ utils.resourceStatusName = function(index) {
throw new Error(`Unknown index: ${index}`); throw new Error(`Unknown index: ${index}`);
}; };
utils.resourceStatus = function(ResourceModel, resourceInfo) { utils.resourceStatus = function(ResourceModel: any, resourceInfo: any) {
if (!ResourceModel) return 'ready'; if (!ResourceModel) return 'ready';
let resourceStatus = 'ready'; let resourceStatus = 'ready';
@ -122,7 +122,7 @@ utils.resourceStatus = function(ResourceModel, resourceInfo) {
return resourceStatus; 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 || !resources) return null;
if (!ResourceModel.isResourceUrl(src)) return null; if (!ResourceModel.isResourceUrl(src)) return null;
@ -151,4 +151,8 @@ utils.imageReplacement = function(ResourceModel, src, resources, resourceBaseUrl
return null; 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;