You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Fixed pasting HTML in Rich Text editor, and improved pasting plain text
This commit is contained in:
		| @@ -953,6 +953,9 @@ packages/lib/fs-driver-node.js.map | |||||||
| packages/lib/htmlUtils.d.ts | packages/lib/htmlUtils.d.ts | ||||||
| packages/lib/htmlUtils.js | packages/lib/htmlUtils.js | ||||||
| packages/lib/htmlUtils.js.map | packages/lib/htmlUtils.js.map | ||||||
|  | packages/lib/htmlUtils.test.d.ts | ||||||
|  | packages/lib/htmlUtils.test.js | ||||||
|  | packages/lib/htmlUtils.test.js.map | ||||||
| packages/lib/import-enex-md-gen.d.ts | packages/lib/import-enex-md-gen.d.ts | ||||||
| packages/lib/import-enex-md-gen.js | packages/lib/import-enex-md-gen.js | ||||||
| packages/lib/import-enex-md-gen.js.map | packages/lib/import-enex-md-gen.js.map | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -939,6 +939,9 @@ packages/lib/fs-driver-node.js.map | |||||||
| packages/lib/htmlUtils.d.ts | packages/lib/htmlUtils.d.ts | ||||||
| packages/lib/htmlUtils.js | packages/lib/htmlUtils.js | ||||||
| packages/lib/htmlUtils.js.map | packages/lib/htmlUtils.js.map | ||||||
|  | packages/lib/htmlUtils.test.d.ts | ||||||
|  | packages/lib/htmlUtils.test.js | ||||||
|  | packages/lib/htmlUtils.test.js.map | ||||||
| packages/lib/import-enex-md-gen.d.ts | packages/lib/import-enex-md-gen.d.ts | ||||||
| packages/lib/import-enex-md-gen.js | packages/lib/import-enex-md-gen.js | ||||||
| packages/lib/import-enex-md-gen.js.map | packages/lib/import-enex-md-gen.js.map | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ const taboverride = require('taboverride'); | |||||||
| import { reg } from '@joplin/lib/registry'; | import { reg } from '@joplin/lib/registry'; | ||||||
| import BaseItem from '@joplin/lib/models/BaseItem'; | import BaseItem from '@joplin/lib/models/BaseItem'; | ||||||
| import setupToolbarButtons from './utils/setupToolbarButtons'; | import setupToolbarButtons from './utils/setupToolbarButtons'; | ||||||
|  | import { plainTextToHtml } from '@joplin/lib/htmlUtils'; | ||||||
| const { themeStyle } = require('@joplin/lib/theme'); | const { themeStyle } = require('@joplin/lib/theme'); | ||||||
| const { clipboard } = require('electron'); | const { clipboard } = require('electron'); | ||||||
| const supportedLocales = require('./supportedLocales'); | const supportedLocales = require('./supportedLocales'); | ||||||
| @@ -1037,6 +1038,10 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		async function onPaste(event: any) { | 		async function onPaste(event: any) { | ||||||
|  | 			// We do not use the default pasting behaviour because the input has | ||||||
|  | 			// to be processed in various ways. | ||||||
|  | 			event.preventDefault(); | ||||||
|  |  | ||||||
| 			const resourceMds = await handlePasteEvent(event); | 			const resourceMds = await handlePasteEvent(event); | ||||||
| 			if (resourceMds.length) { | 			if (resourceMds.length) { | ||||||
| 				const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true })); | 				const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true })); | ||||||
| @@ -1045,23 +1050,25 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { | |||||||
| 				const pastedText = event.clipboardData.getData('text/plain'); | 				const pastedText = event.clipboardData.getData('text/plain'); | ||||||
|  |  | ||||||
| 				if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note | 				if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note | ||||||
| 					event.preventDefault(); |  | ||||||
| 					const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true })); | 					const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true })); | ||||||
| 					editor.insertContent(result.html); | 					editor.insertContent(result.html); | ||||||
| 				} else { // Paste regular text | 				} else { // Paste regular text | ||||||
| 					// HACK: TinyMCE doesn't add an undo step when pasting, for unclear reasons |  | ||||||
| 					// so we manually add it here. We also can't do it immediately it seems, or |  | ||||||
| 					// else nothing is added to the stack, so do it on the next frame. |  | ||||||
|  |  | ||||||
| 					const pastedHtml = event.clipboardData.getData('text/html'); | 					const pastedHtml = event.clipboardData.getData('text/html'); | ||||||
| 					if (pastedHtml) { | 					if (pastedHtml) { // Handles HTML | ||||||
| 						event.preventDefault(); |  | ||||||
| 						const modifiedHtml = await processPastedHtml(pastedHtml); | 						const modifiedHtml = await processPastedHtml(pastedHtml); | ||||||
| 						editor.insertContent(modifiedHtml); | 						editor.insertContent(modifiedHtml); | ||||||
|  | 					} else { // Handles plain text | ||||||
|  | 						pasteAsPlainText(pastedText); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					window.requestAnimationFrame(() => editor.undoManager.add()); | 					// This code before was necessary to get undo working after | ||||||
| 					onChangeHandler(); | 					// 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(); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -1080,6 +1087,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { | |||||||
| 			onChangeHandler(); | 			onChangeHandler(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		function pasteAsPlainText(text: string = null) { | ||||||
|  | 			const pastedText = text === null ? clipboard.readText() : text; | ||||||
|  | 			if (pastedText) { | ||||||
|  | 				editor.insertContent(plainTextToHtml(pastedText)); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		function onKeyDown(event: any) { | 		function onKeyDown(event: any) { | ||||||
| 			// It seems "paste as text" is handled automatically by | 			// It seems "paste as text" is handled automatically by | ||||||
| 			// on Windows so the code below so we need to run the below | 			// on Windows so the code below so we need to run the below | ||||||
| @@ -1092,8 +1106,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { | |||||||
| 			// it here and we don't need to do anything special in onPaste | 			// it here and we don't need to do anything special in onPaste | ||||||
| 			if (!shim.isWindows()) { | 			if (!shim.isWindows()) { | ||||||
| 				if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') { | 				if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') { | ||||||
| 					const pastedText = clipboard.readText(); | 					pasteAsPlainText(); | ||||||
| 					if (pastedText) editor.insertContent(pastedText); |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -135,6 +135,13 @@ export async function processPastedHtml(html: string) { | |||||||
| 	const allImageUrls: string[] = []; | 	const allImageUrls: string[] = []; | ||||||
| 	const mappedResources: Record<string, string> = {}; | 	const mappedResources: Record<string, string> = {}; | ||||||
|  |  | ||||||
|  | 	// When copying text from eg. GitHub, the HTML might contain non-breaking | ||||||
|  | 	// spaces instead of regular spaces. If these non-breaking spaces are | ||||||
|  | 	// inserted into the TinyMCE editor (using insertContent), they will be | ||||||
|  | 	// dropped. So here we convert them to regular spaces. | ||||||
|  | 	// https://stackoverflow.com/a/31790544/561309 | ||||||
|  | 	html = html.replace(/[\u202F\u00A0]/g, ' '); | ||||||
|  |  | ||||||
| 	htmlUtils.replaceImageUrls(html, (src: string) => { | 	htmlUtils.replaceImageUrls(html, (src: string) => { | ||||||
| 		allImageUrls.push(src); | 		allImageUrls.push(src); | ||||||
| 	}); | 	}); | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								packages/lib/htmlUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								packages/lib/htmlUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import { plainTextToHtml } from './htmlUtils'; | ||||||
|  |  | ||||||
|  | describe('htmlUtils', () => { | ||||||
|  |  | ||||||
|  | 	test('should convert a plain text string to its HTML equivalent', () => { | ||||||
|  | 		const testCases = [ | ||||||
|  | 			[ | ||||||
|  | 				'', | ||||||
|  | 				'', | ||||||
|  | 			], | ||||||
|  | 			[ | ||||||
|  | 				'line 1\nline 2', | ||||||
|  | 				'<p>line 1</p><p>line 2</p>', | ||||||
|  | 			], | ||||||
|  | 			[ | ||||||
|  | 				'<img onerror="http://downloadmalware.com"/>', | ||||||
|  | 				'<img onerror="http://downloadmalware.com"/>', | ||||||
|  | 			], | ||||||
|  | 		]; | ||||||
|  |  | ||||||
|  | 		for (const t of testCases) { | ||||||
|  | 			const [input, expected] = t; | ||||||
|  | 			const actual = plainTextToHtml(input); | ||||||
|  | 			expect(actual).toBe(expected); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | }); | ||||||
| @@ -2,6 +2,7 @@ const urlUtils = require('./urlUtils.js'); | |||||||
| const Entities = require('html-entities').AllHtmlEntities; | const Entities = require('html-entities').AllHtmlEntities; | ||||||
| const htmlentities = new Entities().encode; | const htmlentities = new Entities().encode; | ||||||
| const htmlparser2 = require('@joplin/fork-htmlparser2'); | const htmlparser2 = require('@joplin/fork-htmlparser2'); | ||||||
|  | const { escapeHtml } = require('./string-utils.js'); | ||||||
|  |  | ||||||
| // [\s\S] instead of . for multiline matching | // [\s\S] instead of . for multiline matching | ||||||
| // https://stackoverflow.com/a/16119722/561309 | // https://stackoverflow.com/a/16119722/561309 | ||||||
| @@ -153,3 +154,16 @@ class HtmlUtils { | |||||||
| } | } | ||||||
|  |  | ||||||
| export default new HtmlUtils(); | export default new HtmlUtils(); | ||||||
|  |  | ||||||
|  | export function plainTextToHtml(plainText: string): string { | ||||||
|  | 	const lines = plainText | ||||||
|  | 		.replace(/[\n\r]/g, '\n') | ||||||
|  | 		.split('\n'); | ||||||
|  |  | ||||||
|  | 	const lineOpenTag = lines.length > 1 ? '<p>' : ''; | ||||||
|  | 	const lineCloseTag = lines.length > 1 ? '</p>' : ''; | ||||||
|  |  | ||||||
|  | 	return lines | ||||||
|  | 		.map(line => lineOpenTag + escapeHtml(line) + lineCloseTag) | ||||||
|  | 		.join(''); | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user