mirror of
https://github.com/laurent22/joplin.git
synced 2025-03-03 15:32:30 +02:00
Desktop: Allow attaching a file from the Markdown editor for HTML notes
This commit is contained in:
parent
a95a66104d
commit
a7dddaf2c4
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user