mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Merge branch 'dev' of https://github.com/laurent22/joplin into dev
This commit is contained in:
commit
86bace70a5
@ -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
15
.gitignore
vendored
@ -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
|
@ -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) => {
|
||||||
|
@ -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',
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
|
@ -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 {
|
||||||
|
@ -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.
|
||||||
|
53
packages/renderer/MdToHtml/linkReplacement.test.ts
Normal file
53
packages/renderer/MdToHtml/linkReplacement.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
104
packages/renderer/MdToHtml/linkReplacement.ts
Normal file
104
packages/renderer/MdToHtml/linkReplacement.ts
Normal 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}`;
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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}"`;
|
||||||
|
@ -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}`;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
|
@ -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,
|
||||||
|
191
packages/renderer/jest.config.js
Normal file
191
packages/renderer/jest.config.js
Normal 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,
|
||||||
|
};
|
5830
packages/renderer/package-lock.json
generated
5830
packages/renderer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user