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