mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Desktop: Fixes #10733: Fix not-yet-created images lost while editing with the Rich Text Editor (#10734)
This commit is contained in:
parent
9ad1249f11
commit
624bfd9175
@ -2,13 +2,16 @@ import MdToHtml from '@joplin/renderer/MdToHtml';
|
|||||||
const { filename } = require('@joplin/lib/path-utils');
|
const { filename } = require('@joplin/lib/path-utils');
|
||||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
|
import { RenderOptions } from '@joplin/renderer/types';
|
||||||
|
import { isResourceUrl, resourceUrlToId } from '@joplin/lib/models/utils/resourceUtils';
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
const { themeStyle } = require('@joplin/lib/theme');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
function newTestMdToHtml(options: any = null) {
|
function newTestMdToHtml(options: any = null) {
|
||||||
options = {
|
options = {
|
||||||
ResourceModel: {
|
ResourceModel: {
|
||||||
isResourceUrl: () => false,
|
isResourceUrl: isResourceUrl,
|
||||||
|
urlToId: resourceUrlToId,
|
||||||
},
|
},
|
||||||
fsDriver: shim.fsDriver(),
|
fsDriver: shim.fsDriver(),
|
||||||
...options,
|
...options,
|
||||||
@ -39,7 +42,7 @@ describe('MdToHtml', () => {
|
|||||||
// if (mdFilename !== 'sanitize_9.md') continue;
|
// if (mdFilename !== 'sanitize_9.md') continue;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const mdToHtmlOptions: any = {
|
const mdToHtmlOptions: RenderOptions = {
|
||||||
bodyOnly: true,
|
bodyOnly: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -51,6 +54,8 @@ describe('MdToHtml', () => {
|
|||||||
};
|
};
|
||||||
} else if (mdFilename.startsWith('sourcemap_')) {
|
} else if (mdFilename.startsWith('sourcemap_')) {
|
||||||
mdToHtmlOptions.mapsToLine = true;
|
mdToHtmlOptions.mapsToLine = true;
|
||||||
|
} else if (mdFilename.startsWith('resource_')) {
|
||||||
|
mdToHtmlOptions.resources = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdown = await shim.fsDriver().readFile(mdFilePath);
|
const markdown = await shim.fsDriver().readFile(mdFilePath);
|
||||||
|
48
packages/app-cli/tests/html_to_md/resource_placeholder.html
Normal file
48
packages/app-cli/tests/html_to_md/resource_placeholder.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<p>Markdown images:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
With ALT and title:
|
||||||
|
<div
|
||||||
|
class="not-loaded-resource not-loaded-image-resource resource-status-test"
|
||||||
|
data-original-alt="test"
|
||||||
|
data-original-title="testing"
|
||||||
|
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||||
|
>
|
||||||
|
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
With neither ALT nor title:
|
||||||
|
<div
|
||||||
|
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||||
|
data-original-alt=""
|
||||||
|
data-original-title=""
|
||||||
|
data-resource-id="0a25d61cc33e57afa6dde45948c3177f"
|
||||||
|
>
|
||||||
|
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>HTML images:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||||
|
data-original-before=" width="230""
|
||||||
|
data-original-after=" style="border: 32px inset red;"/"
|
||||||
|
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||||
|
>
|
||||||
|
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||||
|
data-original-after="/"
|
||||||
|
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||||
|
>
|
||||||
|
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@ -0,0 +1,9 @@
|
|||||||
|
Markdown images:
|
||||||
|
|
||||||
|
- With ALT and title:![test](:/0415d61cc33e47afa6dde45948c3177f "testing")
|
||||||
|
- With neither ALT nor title:![](:/0a25d61cc33e57afa6dde45948c3177f)
|
||||||
|
|
||||||
|
HTML images:
|
||||||
|
|
||||||
|
- <img width="230" src=":/0415d61cc33e47afa6dde45948c3177f" style="border: 32px inset red;"/>
|
||||||
|
- <img src=":/0415d61cc33e47afa6dde45948c3177f" />
|
@ -0,0 +1,15 @@
|
|||||||
|
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22345" data-original-alt data-original-title="test" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||||
|
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||||
|
		</svg>
|
||||||
|
	"/></div>
|
||||||
|
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22346" data-original-alt="test" data-original-title contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||||
|
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||||
|
		</svg>
|
||||||
|
	"/></div>
|
||||||
|
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22347" data-original-before=" " data-original-after=" class="jop-noMdConv"/" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||||
|
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||||
|
		</svg>
|
||||||
|
	"/></div>
|
@ -0,0 +1,3 @@
|
|||||||
|
![](:/a1test2a1test2a1test2a1test22345 "test")
|
||||||
|
![test](:/a1test2a1test2a1test2a1test22346)
|
||||||
|
<img src=":/a1test2a1test2a1test2a1test22347"/>
|
@ -28,6 +28,7 @@ export default class HtmlToMd {
|
|||||||
bulletListMarker: '-',
|
bulletListMarker: '-',
|
||||||
emDelimiter: '*',
|
emDelimiter: '*',
|
||||||
strongDelimiter: '**',
|
strongDelimiter: '**',
|
||||||
|
allowResourcePlaceholders: true,
|
||||||
|
|
||||||
// If soft-breaks are enabled, lines need to end with two or more spaces for
|
// If soft-breaks are enabled, lines need to end with two or more spaces for
|
||||||
// trailing <br/>s to render. See
|
// trailing <br/>s to render. See
|
||||||
|
@ -3,7 +3,7 @@ import { attributesHtml } from '../../htmlUtils';
|
|||||||
import * as utils from '../../utils';
|
import * as utils from '../../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, ruleOptions.itemIdToUrl);
|
const r = utils.imageReplacement(ruleOptions.ResourceModel, { src, before, after }, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl);
|
||||||
if (typeof r === 'string') return r;
|
if (typeof r === 'string') return r;
|
||||||
if (r) return `<img ${before} ${attributesHtml(r)} ${after}/>`;
|
if (r) return `<img ${before} ${attributesHtml(r)} ${after}/>`;
|
||||||
return `[Image: ${src}]`;
|
return `[Image: ${src}]`;
|
||||||
|
@ -17,7 +17,8 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) {
|
|||||||
|
|
||||||
if (!Resource.isResourceUrl(src) || ruleOptions.plainResourceRendering) return defaultRender(tokens, idx, options, env, self);
|
if (!Resource.isResourceUrl(src) || ruleOptions.plainResourceRendering) return defaultRender(tokens, idx, options, env, self);
|
||||||
|
|
||||||
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl);
|
const alt = token.content;
|
||||||
|
const r = utils.imageReplacement(ruleOptions.ResourceModel, { src, alt, title }, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl);
|
||||||
if (typeof r === 'string') return r;
|
if (typeof r === 'string') return r;
|
||||||
if (r) {
|
if (r) {
|
||||||
const id = r['data-resource-id'];
|
const id = r['data-resource-id'];
|
||||||
@ -35,7 +36,7 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) {
|
|||||||
destroyEditPopupSyntax: ruleOptions.destroyEditPopupSyntax,
|
destroyEditPopupSyntax: ruleOptions.destroyEditPopupSyntax,
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
return `<img data-from-md ${attributesHtml({ ...r, title: title, alt: token.content })} ${js}/>`;
|
return `<img data-from-md ${attributesHtml({ ...r, title: title, alt })} ${js}/>`;
|
||||||
}
|
}
|
||||||
return defaultRender(tokens, idx, options, env, self);
|
return defaultRender(tokens, idx, options, env, self);
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { attributesHtml } from './htmlUtils';
|
||||||
import { ItemIdToUrlHandler, OptionsResourceModel } from './types';
|
import { ItemIdToUrlHandler, OptionsResourceModel } from './types';
|
||||||
|
|
||||||
const Entities = require('html-entities').AllHtmlEntities;
|
const Entities = require('html-entities').AllHtmlEntities;
|
||||||
@ -123,10 +124,17 @@ export const resourceStatus = function(ResourceModel: OptionsResourceModel, reso
|
|||||||
return status;
|
return status;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ImageMarkupData = {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
title: string;
|
||||||
|
}|{ src: string; before: string; after: string };
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
export const imageReplacement = function(ResourceModel: OptionsResourceModel, src: string, resources: any, resourceBaseUrl: string, itemIdToUrl: ItemIdToUrlHandler = null) {
|
export const imageReplacement = function(ResourceModel: OptionsResourceModel, markup: ImageMarkupData, resources: any, resourceBaseUrl: string, itemIdToUrl: ItemIdToUrlHandler = null) {
|
||||||
if (!ResourceModel || !resources) return null;
|
if (!ResourceModel || !resources) return null;
|
||||||
|
|
||||||
|
const src = markup.src;
|
||||||
if (!ResourceModel.isResourceUrl(src)) return null;
|
if (!ResourceModel.isResourceUrl(src)) return null;
|
||||||
|
|
||||||
const resourceId = ResourceModel.urlToId(src);
|
const resourceId = ResourceModel.urlToId(src);
|
||||||
@ -136,7 +144,28 @@ export const imageReplacement = function(ResourceModel: OptionsResourceModel, sr
|
|||||||
|
|
||||||
if (status !== 'ready') {
|
if (status !== 'ready') {
|
||||||
const icon = resourceStatusImage(status);
|
const icon = resourceStatusImage(status);
|
||||||
return `<div class="not-loaded-resource resource-status-${status}" data-resource-id="${resourceId}">` + `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>` + '</div>';
|
|
||||||
|
// Preserve information necessary to restore the original markup when converting
|
||||||
|
// from HTML to markdown.
|
||||||
|
const attrs: Record<string, string> = {
|
||||||
|
class: `not-loaded-resource not-loaded-image-resource resource-status-${status}`,
|
||||||
|
['data-resource-id']: resourceId,
|
||||||
|
};
|
||||||
|
if ('alt' in markup) {
|
||||||
|
attrs['data-original-alt'] = markup.alt;
|
||||||
|
attrs['data-original-title'] = markup.title;
|
||||||
|
} else {
|
||||||
|
attrs['data-original-before'] = markup.before;
|
||||||
|
attrs['data-original-after'] = markup.after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// contenteditable="false": Improves support for the Rich Text Editor -- without this,
|
||||||
|
// users can add content within the <div>, which breaks the html-to-md conversion.
|
||||||
|
return (
|
||||||
|
`<div ${attributesHtml(attrs)} contenteditable="false">`
|
||||||
|
+ `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>`
|
||||||
|
+ '</div>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const mime = resource.mime ? resource.mime.toLowerCase() : '';
|
const mime = resource.mime ? resource.mime.toLowerCase() : '';
|
||||||
if (ResourceModel.isSupportedImageMimeType(mime)) {
|
if (ResourceModel.isSupportedImageMimeType(mime)) {
|
||||||
|
@ -140,6 +140,43 @@ rules.foregroundColor = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converts placeholders for not-loaded resources.
|
||||||
|
rules.resourcePlaceholder = {
|
||||||
|
filter: function (node, options) {
|
||||||
|
if (!options.allowResourcePlaceholders) return false;
|
||||||
|
if (!node.classList || !node.classList.contains('not-loaded-resource')) return false;
|
||||||
|
const isImage = node.classList.contains('not-loaded-image-resource');
|
||||||
|
if (!isImage) return false;
|
||||||
|
|
||||||
|
const resourceId = node.getAttribute('data-resource-id');
|
||||||
|
return resourceId && resourceId.match(/^[a-z0-9]{32}$/);
|
||||||
|
},
|
||||||
|
|
||||||
|
replacement: function (_content, node) {
|
||||||
|
const htmlBefore = node.getAttribute('data-original-before') || '';
|
||||||
|
const htmlAfter = node.getAttribute('data-original-after') || '';
|
||||||
|
const isHtml = htmlBefore || htmlAfter;
|
||||||
|
const resourceId = node.getAttribute('data-resource-id');
|
||||||
|
if (isHtml) {
|
||||||
|
const attrs = [
|
||||||
|
htmlBefore.trim(),
|
||||||
|
`src=":/${resourceId}"`,
|
||||||
|
htmlAfter.trim(),
|
||||||
|
].filter(a => !!a);
|
||||||
|
|
||||||
|
return `<img ${attrs.join(' ')}>`;
|
||||||
|
} else {
|
||||||
|
const originalAltText = node.getAttribute('data-original-alt') || '';
|
||||||
|
const title = node.getAttribute('data-original-title');
|
||||||
|
return imageMarkdownFromAttributes({
|
||||||
|
alt: originalAltText,
|
||||||
|
title,
|
||||||
|
src: `:/${resourceId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// END Joplin format support
|
// END Joplin format support
|
||||||
// ==============================
|
// ==============================
|
||||||
@ -510,6 +547,14 @@ rules.code = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imageMarkdownFromAttributes(attributes) {
|
||||||
|
var alt = attributes.alt || ''
|
||||||
|
var src = filterLinkHref(attributes.src || '')
|
||||||
|
var title = attributes.title || ''
|
||||||
|
var titlePart = title ? ' "' + filterImageTitle(title) + '"' : ''
|
||||||
|
return src ? '![' + alt.replace(/([[\]])/g, '\\$1') + ']' + '(' + src + titlePart + ')' : ''
|
||||||
|
}
|
||||||
|
|
||||||
function imageMarkdownFromNode(node, options = null) {
|
function imageMarkdownFromNode(node, options = null) {
|
||||||
options = Object.assign({}, {
|
options = Object.assign({}, {
|
||||||
preserveImageTagsWithSize: false,
|
preserveImageTagsWithSize: false,
|
||||||
@ -519,11 +564,11 @@ function imageMarkdownFromNode(node, options = null) {
|
|||||||
return node.outerHTML;
|
return node.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
var alt = node.alt || ''
|
return imageMarkdownFromAttributes({
|
||||||
var src = filterLinkHref(node.getAttribute('src') || '')
|
alt: node.alt,
|
||||||
var title = node.title || ''
|
src: node.getAttribute('src'),
|
||||||
var titlePart = title ? ' "' + filterImageTitle(title) + '"' : ''
|
title: node.title,
|
||||||
return src ? '![' + alt.replace(/([[\]])/g, '\\$1') + ']' + '(' + src + titlePart + ')' : ''
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function imageUrlFromSource(node) {
|
function imageUrlFromSource(node) {
|
||||||
|
Loading…
Reference in New Issue
Block a user