You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Allow attaching a file from the Markdown editor for HTML notes
This commit is contained in:
		| @@ -66,8 +66,10 @@ describe('MdToHtml', () => { | ||||
| 					actualHtml, | ||||
| 					'--------------------------------- Raw:', | ||||
| 					actualHtml.split('\n'), | ||||
| 					'--------------------------------- Expected:', | ||||
| 					'--------------------------------- Expected (Lines)', | ||||
| 					expectedHtml.split('\n'), | ||||
| 					'--------------------------------- Expected (Text)', | ||||
| 					expectedHtml, | ||||
| 					'--------------------------------------------', | ||||
| 					'', | ||||
| 				]; | ||||
|   | ||||
							
								
								
									
										7
									
								
								packages/app-cli/tests/md_to_html/sanitize_links.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/app-cli/tests/md_to_html/sanitize_links.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| <p><a href=":/62d16d1c1e28418da6624fa8742a7ed0" class="jop-noMdConv">Resource link</a></p> | ||||
| <p><a href="https://example.com/ok" class="jop-noMdConv">ok</a></p> | ||||
| <p><a href="http://example.com/ok" class="jop-noMdConv">ok</a></p> | ||||
| <p><a href="mailto:name@email.com" class="jop-noMdConv">ok</a></p> | ||||
| <p><a href="joplin://62d16d1c1e28418da6624fa8742a7ed0" class="jop-noMdConv">ok</a></p> | ||||
| <p><a href="#" class="jop-noMdConv">not ok</a></p> | ||||
| <p><a href="#" class="jop-noMdConv">not ok</a></p> | ||||
							
								
								
									
										13
									
								
								packages/app-cli/tests/md_to_html/sanitize_links.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/app-cli/tests/md_to_html/sanitize_links.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| <a href=":/62d16d1c1e28418da6624fa8742a7ed0">Resource link</a> | ||||
|  | ||||
| <a href="https://example.com/ok">ok</a> | ||||
|  | ||||
| <a href="http://example.com/ok">ok</a> | ||||
|  | ||||
| <a href="mailto:name@email.com">ok</a> | ||||
|  | ||||
| <a href="joplin://62d16d1c1e28418da6624fa8742a7ed0">ok</a> | ||||
|  | ||||
| <a href="file:///etc/passwd">not ok</a> | ||||
|  | ||||
| <a href="data://blabla">not ok</a> | ||||
| @@ -136,7 +136,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor | ||||
| 						editorRef.current.insertAtCursor(cmd.value.markdownTags.join('\n')); | ||||
| 					} else if (cmd.value.type === 'files') { | ||||
| 						const pos = cursorPositionToTextOffset(editorRef.current.getCursor(), props.content); | ||||
| 						const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, { createFileURL: !!cmd.value.createFileURL, position: pos }); | ||||
| 						const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, { | ||||
| 							createFileURL: !!cmd.value.createFileURL, | ||||
| 							position: pos, | ||||
| 							markupLanguage: props.contentMarkupLanguage, | ||||
| 						}); | ||||
| 						editorRef.current.updateBody(newBody); | ||||
| 					} else { | ||||
| 						reg.logger().warn('CodeMirror: unsupported drop item: ', cmd); | ||||
| @@ -214,7 +218,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor | ||||
| 							const cursor = editorRef.current.getCursor(); | ||||
| 							const pos = cursorPositionToTextOffset(cursor, props.content); | ||||
|  | ||||
| 							const newBody = await commandAttachFileToBody(props.content, null, { position: pos }); | ||||
| 							const newBody = await commandAttachFileToBody(props.content, null, { position: pos, markupLanguage: props.contentMarkupLanguage }); | ||||
| 							if (newBody) editorRef.current.updateBody(newBody); | ||||
| 						}, | ||||
| 						textNumberedList: () => { | ||||
| @@ -255,7 +259,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor | ||||
| 			}, | ||||
| 		}; | ||||
| 		// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied | ||||
| 	}, [props.content, props.visiblePanes, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]); | ||||
| 	}, [props.content, props.visiblePanes, props.contentMarkupLanguage, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]); | ||||
|  | ||||
| 	const onEditorPaste = useCallback(async (event: any = null) => { | ||||
| 		const resourceMds = await getResourcesFromPasteEvent(event); | ||||
|   | ||||
| @@ -151,6 +151,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor | ||||
| 		editorCopyText, editorCutText, editorPaste, | ||||
| 		editorContent: props.content, | ||||
| 		visiblePanes: props.visiblePanes, | ||||
| 		contentMarkupLanguage: props.contentMarkupLanguage, | ||||
| 	}); | ||||
|  | ||||
| 	useImperativeHandle(ref, () => { | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import dialogs from '../../../../dialogs'; | ||||
| import { EditorCommandType } from '@joplin/editor/types'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'; | ||||
| import { MarkupLanguage } from '@joplin/renderer'; | ||||
|  | ||||
| const logger = Logger.create('CodeMirror 6 commands'); | ||||
|  | ||||
| @@ -40,6 +41,7 @@ interface Props { | ||||
| 	selectionRange: { from: number; to: number }; | ||||
|  | ||||
| 	visiblePanes: string[]; | ||||
| 	contentMarkupLanguage: MarkupLanguage; | ||||
| } | ||||
|  | ||||
| const useEditorCommands = (props: Props) => { | ||||
| @@ -57,7 +59,7 @@ const useEditorCommands = (props: Props) => { | ||||
| 					editorRef.current.insertText(cmd.markdownTags.join('\n')); | ||||
| 				} else if (cmd.type === 'files') { | ||||
| 					const pos = props.selectionRange.from; | ||||
| 					const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos }); | ||||
| 					const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos, markupLanguage: props.contentMarkupLanguage }); | ||||
| 					editorRef.current.updateBody(newBody); | ||||
| 				} else { | ||||
| 					logger.warn('CodeMirror: unsupported drop item: ', cmd); | ||||
| @@ -92,7 +94,7 @@ const useEditorCommands = (props: Props) => { | ||||
| 			insertText: (value: any) => editorRef.current.insertText(value), | ||||
| 			attachFile: async () => { | ||||
| 				const newBody = await commandAttachFileToBody( | ||||
| 					props.editorContent, null, { position: props.selectionRange.from }, | ||||
| 					props.editorContent, null, { position: props.selectionRange.from, markupLanguage: props.contentMarkupLanguage }, | ||||
| 				); | ||||
| 				if (newBody) { | ||||
| 					editorRef.current.updateBody(newBody); | ||||
| @@ -129,7 +131,7 @@ const useEditorCommands = (props: Props) => { | ||||
| 	}, [ | ||||
| 		props.visiblePanes, props.editorContent, props.editorCopyText, props.editorCutText, props.editorPaste, | ||||
| 		props.selectionRange, | ||||
|  | ||||
| 		props.contentMarkupLanguage, | ||||
| 		props.webviewRef, editorRef, | ||||
| 	]); | ||||
| }; | ||||
|   | ||||
| @@ -2,9 +2,14 @@ import { CommandDeclaration } from '@joplin/lib/services/CommandService'; | ||||
| import { _ } from '@joplin/lib/locale'; | ||||
| import { joplinCommandToTinyMceCommands } from './NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands'; | ||||
|  | ||||
| const workWithHtmlNotes = [ | ||||
| 	'attachFile', | ||||
| ]; | ||||
|  | ||||
| export const enabledCondition = (commandName: string) => { | ||||
| 	const markdownEditorOnly = !Object.keys(joplinCommandToTinyMceCommands).includes(commandName); | ||||
| 	return `(!modalDialogVisible || gotoAnythingVisible) ${markdownEditorOnly ? '&& markdownEditorPaneVisible' : ''} && oneNoteSelected && noteIsMarkdown && !noteIsReadOnly`; | ||||
| 	const noteMustBeMarkdown = !workWithHtmlNotes.includes(commandName); | ||||
| 	return `(!modalDialogVisible || gotoAnythingVisible) ${markdownEditorOnly ? '&& markdownEditorPaneVisible' : ''} && oneNoteSelected ${noteMustBeMarkdown ? '&& noteIsMarkdown' : ''} && !noteIsReadOnly`; | ||||
| }; | ||||
|  | ||||
| const declarations: CommandDeclaration[] = [ | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import htmlUtils from '@joplin/lib/htmlUtils'; | ||||
| import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import { fileUriToPath } from '@joplin/utils/url'; | ||||
| import { MarkupLanguage } from '@joplin/renderer'; | ||||
| const joplinRendererUtils = require('@joplin/renderer').utils; | ||||
| const { clipboard } = require('electron'); | ||||
| const mimeUtils = require('@joplin/lib/mime-utils.js').mime; | ||||
| @@ -62,6 +63,7 @@ export async function commandAttachFileToBody(body: string, filePaths: string[] | ||||
| 	options = { | ||||
| 		createFileURL: false, | ||||
| 		position: 0, | ||||
| 		markupLanguage: MarkupLanguage.Markdown, | ||||
| 		...options, | ||||
| 	}; | ||||
|  | ||||
| @@ -79,6 +81,7 @@ export async function commandAttachFileToBody(body: string, filePaths: string[] | ||||
| 			const newBody = await shim.attachFileToNoteBody(body, filePath, options.position, { | ||||
| 				createFileURL: options.createFileURL, | ||||
| 				resizeLargeImages: Setting.value('imageResizing'), | ||||
| 				markupLanguage: options.markupLanguage, | ||||
| 			}); | ||||
|  | ||||
| 			if (!newBody) { | ||||
|   | ||||
| @@ -727,7 +727,7 @@ class NoteScreenComponent extends BaseScreenComponent { | ||||
|  | ||||
| 		resource = await Resource.save(resource, { isNew: true }); | ||||
|  | ||||
| 		const resourceTag = Resource.markdownTag(resource); | ||||
| 		const resourceTag = Resource.markupTag(resource); | ||||
|  | ||||
| 		const newNote = { ...this.state.note }; | ||||
|  | ||||
|   | ||||
| @@ -16,6 +16,8 @@ import itemCanBeEncrypted from './utils/itemCanBeEncrypted'; | ||||
| import { getEncryptionEnabled } from '../services/synchronizer/syncInfoUtils'; | ||||
| import ShareService from '../services/share/ShareService'; | ||||
| import { SaveOptions } from './utils/types'; | ||||
| import { MarkupLanguage } from '@joplin/renderer'; | ||||
| import { htmlentities } from '@joplin/utils/html'; | ||||
|  | ||||
| export default class Resource extends BaseItem { | ||||
|  | ||||
| @@ -231,18 +233,28 @@ export default class Resource extends BaseItem { | ||||
| 		return { path: encryptedPath, resource: resourceCopy }; | ||||
| 	} | ||||
|  | ||||
| 	public static markdownTag(resource: any) { | ||||
| 	public static markupTag(resource: any, markupLanguage: MarkupLanguage = MarkupLanguage.Markdown) { | ||||
| 		let tagAlt = resource.alt ? resource.alt : resource.title; | ||||
| 		if (!tagAlt) tagAlt = ''; | ||||
| 		const lines = []; | ||||
| 		if (Resource.isSupportedImageMimeType(resource.mime)) { | ||||
| 			lines.push('`); | ||||
| 			if (markupLanguage === MarkupLanguage.Markdown) { | ||||
| 				lines.push('`); | ||||
| 			} else { | ||||
| 				const altHtml = tagAlt ? `alt="${htmlentities(tagAlt)}"` : ''; | ||||
| 				lines.push(`<img src=":/${resource.id}" ${altHtml}/>`); | ||||
| 			} | ||||
| 		} else { | ||||
| 			lines.push('['); | ||||
| 			lines.push(markdownUtils.escapeTitleText(tagAlt)); | ||||
| 			lines.push(`](:/${resource.id})`); | ||||
| 			if (markupLanguage === MarkupLanguage.Markdown) { | ||||
| 				lines.push('['); | ||||
| 				lines.push(markdownUtils.escapeTitleText(tagAlt)); | ||||
| 				lines.push(`](:/${resource.id})`); | ||||
| 			} else { | ||||
| 				const altHtml = tagAlt ? `alt="${htmlentities(tagAlt)}"` : ''; | ||||
| 				lines.push(`<a href=":/${resource.id}" ${altHtml}>${htmlentities(tagAlt ? tagAlt : resource.id)}</a>`); | ||||
| 			} | ||||
| 		} | ||||
| 		return lines.join(''); | ||||
| 	} | ||||
| @@ -444,7 +456,7 @@ export default class Resource extends BaseItem { | ||||
|  | ||||
| 		await Note.save({ | ||||
| 			title: _('Attachment conflict: "%s"', resource.title), | ||||
| 			body: _('There was a [conflict](%s) on the attachment below.\n\n%s', 'https://joplinapp.org/help/apps/conflict', Resource.markdownTag(conflictResource)), | ||||
| 			body: _('There was a [conflict](%s) on the attachment below.\n\n%s', 'https://joplinapp.org/help/apps/conflict', Resource.markupTag(conflictResource)), | ||||
| 			parent_id: await this.resourceConflictFolderId(), | ||||
| 		}, { changeSource: ItemChange.SOURCE_SYNC }); | ||||
| 	} | ||||
|   | ||||
| @@ -63,7 +63,7 @@ describe('services/ResourceService', () => { | ||||
|  | ||||
| 		await service.indexNoteResources(); | ||||
|  | ||||
| 		await Note.save({ id: note2.id, body: Resource.markdownTag(resource1) }); | ||||
| 		await Note.save({ id: note2.id, body: Resource.markupTag(resource1) }); | ||||
|  | ||||
| 		await service.indexNoteResources(); | ||||
|  | ||||
|   | ||||
| @@ -332,7 +332,7 @@ function shimInit(options = null) { | ||||
| 	}; | ||||
|  | ||||
| 	shim.attachFileToNoteBody = async function(noteBody, filePath, position = null, options = null) { | ||||
| 		options = { createFileURL: false, ...options }; | ||||
| 		options = { createFileURL: false, markupLanguage: 1, ...options }; | ||||
|  | ||||
| 		const { basename } = require('path'); | ||||
| 		const { escapeTitleText } = require('./markdownUtils').default; | ||||
| @@ -353,7 +353,7 @@ function shimInit(options = null) { | ||||
| 		if (noteBody && position) newBody.push(noteBody.substr(0, position)); | ||||
|  | ||||
| 		if (!options.createFileURL) { | ||||
| 			newBody.push(Resource.markdownTag(resource)); | ||||
| 			newBody.push(Resource.markupTag(resource, options.markupLanguage)); | ||||
| 		} else { | ||||
| 			const filename = escapeTitleText(basename(filePath)); // to get same filename as standard drag and drop | ||||
| 			const fileURL = `[${filename}](${toFileProtocolPath(filePath)})`; | ||||
| @@ -366,6 +366,7 @@ function shimInit(options = null) { | ||||
| 	}; | ||||
|  | ||||
| 	shim.attachFileToNote = async function(note, filePath, position = null, options = null) { | ||||
| 		if (note.markup_language) options.markupLanguage = note.markup_language; | ||||
| 		const newBody = await shim.attachFileToNoteBody(note.body, filePath, position, options); | ||||
| 		if (!newBody) return null; | ||||
|  | ||||
|   | ||||
| @@ -159,12 +159,14 @@ class HtmlUtils { | ||||
| 			.replace(/</g, '<'); | ||||
| 	} | ||||
|  | ||||
| 	// This is tested in sanitize_links.md | ||||
| 	private isAcceptedUrl(url: string, allowedFilePrefixes: string[]): boolean { | ||||
| 		url = url.toLowerCase(); | ||||
| 		if (url.startsWith('https://') || | ||||
| 			url.startsWith('http://') || | ||||
| 			url.startsWith('mailto://') || | ||||
| 			url.startsWith('mailto:') || | ||||
| 			url.startsWith('joplin://') || | ||||
| 			!!url.match(/:\/[0-9a-zA-Z]{32}/) || | ||||
| 			// We also allow anchors but only with a specific set of a characters. | ||||
| 			// Fixes https://github.com/laurent22/joplin/issues/8286 | ||||
| 			!!url.match(/^#[a-zA-Z0-9-]+$/)) return true; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user