You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
1 Commits
server-v3.
...
paste_from
Author | SHA1 | Date | |
---|---|---|---|
|
377d02e19d |
@@ -3,7 +3,7 @@ import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHand
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||
import { CommandValue } from '../../utils/types';
|
||||
import { usePrevious, cursorPositionToTextOffset } from './utils';
|
||||
@@ -268,7 +268,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
}, [props.content, props.visiblePanes, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
|
||||
|
||||
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
const resourceMds = await getResourcesFromPasteEvent(event);
|
||||
if (!resourceMds.length) return;
|
||||
if (editorRef.current) {
|
||||
editorRef.current.replaceSelection(resourceMds.join('\n'));
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos } from '../../utils/types';
|
||||
import { resourcesStatus, commandAttachFileToBody, handlePasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
|
||||
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
|
||||
import useScroll from './utils/useScroll';
|
||||
import styles_ from './styles';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -1064,38 +1064,43 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// to be processed in various ways.
|
||||
event.preventDefault();
|
||||
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
if (resourceMds.length) {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else {
|
||||
const pastedText = event.clipboardData.getData('text/plain');
|
||||
const pastedText = event.clipboardData.getData('text/plain');
|
||||
|
||||
// event.clipboardData.getData('text/html') wraps the
|
||||
// content with <html><body></body></html>, which seems to
|
||||
// be not supported in editor.insertContent().
|
||||
//
|
||||
// when pasting text with Ctrl+Shift+V, the format should be
|
||||
// ignored. In this case,
|
||||
// event.clopboardData.getData('text/html') returns an empty
|
||||
// string, but the clipboard.readHTML() still returns the
|
||||
// formatted text.
|
||||
const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : '';
|
||||
|
||||
// We should only process the images if there is no plain text or
|
||||
// HTML text in the clipboard. This is because certain applications,
|
||||
// such as Word, are going to add multiple versions of the copied
|
||||
// data to the clipboard - one with the text formatted as HTML, and
|
||||
// one with the text as an image. In that case, we need to ignore
|
||||
// the image and only process the HTML.
|
||||
|
||||
if (!pastedText && !pastedHtml) {
|
||||
const resourceMds = await getResourcesFromPasteEvent(event);
|
||||
if (resourceMds.length) {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
}
|
||||
} else {
|
||||
if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else { // Paste regular text
|
||||
// event.clipboardData.getData('text/html') wraps the content with <html><body></body></html>,
|
||||
// which seems to be not supported in editor.insertContent().
|
||||
//
|
||||
// when pasting text with Ctrl+Shift+V, the format should be ignored.
|
||||
// In this case, event.clopboardData.getData('text/html') returns an empty string, but the clipboard.readHTML() still returns the formatted text.
|
||||
const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : '';
|
||||
if (pastedHtml) { // Handles HTML
|
||||
const modifiedHtml = await processPastedHtml(pastedHtml);
|
||||
editor.insertContent(modifiedHtml);
|
||||
} else { // Handles plain text
|
||||
pasteAsPlainText(pastedText);
|
||||
}
|
||||
|
||||
// This code before was necessary to get undo working after
|
||||
// pasting but it seems it's no longer necessary, so
|
||||
// removing it for now. We also couldn't do it immediately
|
||||
// it seems, or else nothing is added to the stack, so do it
|
||||
// on the next frame.
|
||||
//
|
||||
// window.requestAnimationFrame(() =>
|
||||
// editor.undoManager.add()); onChangeHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import Resource from '@joplin/lib/models/Resource';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import htmlUtils from '@joplin/lib/htmlUtils';
|
||||
import rendererHtmlUtils from '@joplin/renderer/htmlUtils';
|
||||
import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const { fileUriToPath } = require('@joplin/lib/urlUtils');
|
||||
const joplinRendererUtils = require('@joplin/renderer').utils;
|
||||
@@ -107,7 +107,7 @@ export function resourcesStatus(resourceInfos: any) {
|
||||
return joplinRendererUtils.resourceStatusName(lowestIndex);
|
||||
}
|
||||
|
||||
export async function handlePasteEvent(event: any) {
|
||||
export async function getResourcesFromPasteEvent(event: any) {
|
||||
const output = [];
|
||||
const formats = clipboard.availableFormats();
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
@@ -176,9 +176,9 @@ export async function processPastedHtml(html: string) {
|
||||
}
|
||||
}
|
||||
|
||||
return rendererHtmlUtils.sanitizeHtml(
|
||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(
|
||||
htmlUtils.replaceImageUrls(html, (src: string) => {
|
||||
return mappedResources[src];
|
||||
})
|
||||
);
|
||||
));
|
||||
}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { RuleOptions } from '../../MdToHtml';
|
||||
import htmlUtils from '../../htmlUtils';
|
||||
import { attributesHtml } 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, ruleOptions.itemIdToUrl);
|
||||
if (typeof r === 'string') return r;
|
||||
if (r) return `<img ${before} ${htmlUtils.attributesHtml(r)} ${after}/>`;
|
||||
if (r) return `<img ${before} ${attributesHtml(r)} ${after}/>`;
|
||||
return `[Image: ${src}]`;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { RuleOptions } from '../../MdToHtml';
|
||||
import htmlUtils from '../../htmlUtils';
|
||||
import { attributesHtml } from '../../htmlUtils';
|
||||
import utils from '../../utils';
|
||||
import createEventHandlingAttrs from '../createEventHandlingAttrs';
|
||||
|
||||
@@ -25,7 +25,7 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) {
|
||||
postMessageSyntax: ruleOptions.postMessageSyntax ?? 'void',
|
||||
}, null);
|
||||
|
||||
return `<img data-from-md ${htmlUtils.attributesHtml({ ...r, title: title, alt: token.content })} ${js}/>`;
|
||||
return `<img data-from-md ${attributesHtml({ ...r, title: title, alt: token.content })} ${js}/>`;
|
||||
}
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import htmlUtils from './htmlUtils';
|
||||
import htmlUtils, { extractHtmlBody } from './htmlUtils';
|
||||
|
||||
describe('htmlUtils', () => {
|
||||
|
||||
@@ -29,4 +29,26 @@ describe('htmlUtils', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should extract the HTML body', () => {
|
||||
const testCases: [string, string][] = [
|
||||
[
|
||||
'Just <b>testing</b>',
|
||||
'Just <b>testing</b>',
|
||||
],
|
||||
[
|
||||
'',
|
||||
'',
|
||||
],
|
||||
[
|
||||
'<html><head></head><meta bla><body>Here is the body<img src="test.png"/></body></html>',
|
||||
'Here is the body<img src="test.png"/>',
|
||||
],
|
||||
];
|
||||
|
||||
for (const [input, expected] of testCases) {
|
||||
const actual = extractHtmlBody(input);
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -34,24 +34,28 @@ interface SanitizeHtmlOptions {
|
||||
addNoMdConvClass: boolean;
|
||||
}
|
||||
|
||||
class HtmlUtils {
|
||||
export const attributesHtml = (attr: Record<string, string>) => {
|
||||
const output = [];
|
||||
|
||||
public attributesHtml(attr: Record<string, string>) {
|
||||
const output = [];
|
||||
for (const n in attr) {
|
||||
if (!attr.hasOwnProperty(n)) continue;
|
||||
|
||||
for (const n in attr) {
|
||||
if (!attr.hasOwnProperty(n)) continue;
|
||||
|
||||
if (!attr[n]) {
|
||||
output.push(n);
|
||||
} else {
|
||||
output.push(`${n}="${htmlentities(attr[n])}"`);
|
||||
}
|
||||
if (!attr[n]) {
|
||||
output.push(n);
|
||||
} else {
|
||||
output.push(`${n}="${htmlentities(attr[n])}"`);
|
||||
}
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
return output.join(' ');
|
||||
};
|
||||
|
||||
export const isSelfClosingTag = (tagName: string) => {
|
||||
return selfClosingElements.includes(tagName.toLowerCase());
|
||||
};
|
||||
|
||||
class HtmlUtils {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
public processImageTags(html: string, callback: Function) {
|
||||
if (!html) return '';
|
||||
@@ -70,7 +74,7 @@ class HtmlUtils {
|
||||
}
|
||||
|
||||
if (action.type === 'setAttributes') {
|
||||
const attrHtml = this.attributesHtml(action.attrs);
|
||||
const attrHtml = attributesHtml(action.attrs);
|
||||
return `<img${before}${attrHtml}${after}>`;
|
||||
}
|
||||
|
||||
@@ -103,7 +107,7 @@ class HtmlUtils {
|
||||
}
|
||||
|
||||
if (action.type === 'setAttributes') {
|
||||
const attrHtml = this.attributesHtml(action.attrs);
|
||||
const attrHtml = attributesHtml(action.attrs);
|
||||
return `<img${before}${attrHtml}${after}>`;
|
||||
}
|
||||
|
||||
@@ -111,10 +115,6 @@ class HtmlUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public isSelfClosingTag(tagName: string) {
|
||||
return selfClosingElements.includes(tagName.toLowerCase());
|
||||
}
|
||||
|
||||
public stripHtml(html: string) {
|
||||
const output: string[] = [];
|
||||
|
||||
@@ -274,9 +274,9 @@ class HtmlUtils {
|
||||
attrs['href'] = '#';
|
||||
}
|
||||
|
||||
let attrHtml = this.attributesHtml(attrs);
|
||||
let attrHtml = attributesHtml(attrs);
|
||||
if (attrHtml) attrHtml = ` ${attrHtml}`;
|
||||
const closingSign = this.isSelfClosingTag(name) ? '/>' : '>';
|
||||
const closingSign = isSelfClosingTag(name) ? '/>' : '>';
|
||||
output.push(`<${name}${attrHtml}${closingSign}`);
|
||||
},
|
||||
|
||||
@@ -319,7 +319,7 @@ class HtmlUtils {
|
||||
|
||||
if (disallowedTagDepth) return;
|
||||
|
||||
if (this.isSelfClosingTag(name)) return;
|
||||
if (isSelfClosingTag(name)) return;
|
||||
output.push(`</${name}>`);
|
||||
},
|
||||
|
||||
@@ -334,4 +334,53 @@ class HtmlUtils {
|
||||
|
||||
}
|
||||
|
||||
const makeHtmlTag = (name: string, attrs: Record<string, string>) => {
|
||||
let attrHtml = attributesHtml(attrs);
|
||||
if (attrHtml) attrHtml = ` ${attrHtml}`;
|
||||
const closingSign = isSelfClosingTag(name) ? '/>' : '>';
|
||||
return `<${name}${attrHtml}${closingSign}`;
|
||||
};
|
||||
|
||||
// Will return either the content of the <BODY> tag if it exists, or the whole
|
||||
// HTML (which would be a fragment of HTML)
|
||||
export const extractHtmlBody = (html: string) => {
|
||||
let inBody = false;
|
||||
let bodyFound = false;
|
||||
const output: string[] = [];
|
||||
|
||||
const parser = new htmlparser2.Parser({
|
||||
|
||||
onopentag: (name: string, attrs: Record<string, string>) => {
|
||||
if (name === 'body') {
|
||||
inBody = true;
|
||||
bodyFound = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (inBody) {
|
||||
output.push(makeHtmlTag(name, attrs));
|
||||
}
|
||||
},
|
||||
|
||||
ontext: (encodedText: string) => {
|
||||
if (inBody) output.push(encodedText);
|
||||
},
|
||||
|
||||
onclosetag: (name: string) => {
|
||||
if (inBody && name === 'body') inBody = false;
|
||||
|
||||
if (inBody) {
|
||||
if (isSelfClosingTag(name)) return;
|
||||
output.push(`</${name}>`);
|
||||
}
|
||||
},
|
||||
|
||||
}, { decodeEntities: false });
|
||||
|
||||
parser.write(html);
|
||||
parser.end();
|
||||
|
||||
return bodyFound ? output.join('') : html;
|
||||
};
|
||||
|
||||
export default new HtmlUtils();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { unique } from '@joplin/lib/ArrayUtils';
|
||||
import htmlUtils from '@joplin/renderer/htmlUtils';
|
||||
import { attributesHtml, isSelfClosingTag } from '@joplin/renderer/htmlUtils';
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
const htmlparser2 = require('@joplin/fork-htmlparser2');
|
||||
@@ -106,9 +106,9 @@ export default (html: string, _languageCode: string, translations: Record<string
|
||||
state.translateStack.push(name);
|
||||
}
|
||||
|
||||
let attrHtml = htmlUtils.attributesHtml(attrs);
|
||||
let attrHtml = attributesHtml(attrs);
|
||||
if (attrHtml) attrHtml = ` ${attrHtml}`;
|
||||
const closingSign = htmlUtils.isSelfClosingTag(name) ? '/>' : '>';
|
||||
const closingSign = isSelfClosingTag(name) ? '/>' : '>';
|
||||
|
||||
pushContent(state, `<${name}${attrHtml}${closingSign}`);
|
||||
state.translateIsOpening = false;
|
||||
@@ -133,7 +133,7 @@ export default (html: string, _languageCode: string, translations: Record<string
|
||||
|
||||
if (name === 'script') state.inScript = false;
|
||||
|
||||
if (htmlUtils.isSelfClosingTag(name)) return;
|
||||
if (isSelfClosingTag(name)) return;
|
||||
pushContent(state, `</${name}>`);
|
||||
},
|
||||
|
||||
|
Reference in New Issue
Block a user