mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Desktop: Fixes #9512: Pasting rich text in the RTE sometimes result in invalid markup
This commit is contained in:
parent
b69d752734
commit
3e13a95053
@ -294,6 +294,7 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.js
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.js
|
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/types.js
|
packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -274,6 +274,7 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.js
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.js
|
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/types.js
|
packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||||
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
|
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { EditorCommand, NoteBodyEditorProps, NoteBodyEditorRef } from '../../../utils/types';
|
import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditorRef } from '../../../utils/types';
|
||||||
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
||||||
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
||||||
import { CommandValue } from '../../../utils/types';
|
import { CommandValue } from '../../../utils/types';
|
||||||
@ -29,7 +29,6 @@ const debounce = require('debounce');
|
|||||||
|
|
||||||
import { reg } from '@joplin/lib/registry';
|
import { reg } from '@joplin/lib/registry';
|
||||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||||
import { MarkupToHtmlOptions } from '../../../utils/useMarkupToHtml';
|
|
||||||
import useStyles from '../utils/useStyles';
|
import useStyles from '../utils/useStyles';
|
||||||
import useContextMenu from '../utils/useContextMenu';
|
import useContextMenu from '../utils/useContextMenu';
|
||||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
|
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
|
||||||
|
|
||||||
import { EditorCommand, NoteBodyEditorProps, NoteBodyEditorRef, OnChangeEvent } from '../../../utils/types';
|
import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditorRef, OnChangeEvent } from '../../../utils/types';
|
||||||
import { getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
import { getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
||||||
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
||||||
import NoteTextViewer from '../../../../NoteTextViewer';
|
import NoteTextViewer from '../../../../NoteTextViewer';
|
||||||
@ -16,7 +16,6 @@ import { MarkupToHtml } from '@joplin/renderer';
|
|||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
import { reg } from '@joplin/lib/registry';
|
import { reg } from '@joplin/lib/registry';
|
||||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||||
import { MarkupToHtmlOptions } from '../../../utils/useMarkupToHtml';
|
|
||||||
import { EditorKeymap, EditorLanguageType, EditorSettings } from '@joplin/editor/types';
|
import { EditorKeymap, EditorLanguageType, EditorSettings } from '@joplin/editor/types';
|
||||||
import useStyles from '../utils/useStyles';
|
import useStyles from '../utils/useStyles';
|
||||||
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
|
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||||
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos } from '../../utils/types';
|
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos, HtmlToMarkdownHandler } from '../../utils/types';
|
||||||
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
|
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
|
||||||
import useScroll from './utils/useScroll';
|
import useScroll from './utils/useScroll';
|
||||||
import styles_ from './styles';
|
import styles_ from './styles';
|
||||||
@ -20,7 +20,6 @@ import BaseItem from '@joplin/lib/models/BaseItem';
|
|||||||
import setupToolbarButtons from './utils/setupToolbarButtons';
|
import setupToolbarButtons from './utils/setupToolbarButtons';
|
||||||
import { plainTextToHtml } from '@joplin/lib/htmlUtils';
|
import { plainTextToHtml } from '@joplin/lib/htmlUtils';
|
||||||
import openEditDialog from './utils/openEditDialog';
|
import openEditDialog from './utils/openEditDialog';
|
||||||
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
|
|
||||||
import { themeStyle } from '@joplin/lib/theme';
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
import { loadScript } from '../../../utils/loadScript';
|
import { loadScript } from '../../../utils/loadScript';
|
||||||
import bridge from '../../../../services/bridge';
|
import bridge from '../../../../services/bridge';
|
||||||
@ -30,25 +29,11 @@ import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCo
|
|||||||
import shouldPasteResources from './utils/shouldPasteResources';
|
import shouldPasteResources from './utils/shouldPasteResources';
|
||||||
import lightTheme from '@joplin/lib/themes/light';
|
import lightTheme from '@joplin/lib/themes/light';
|
||||||
import { Options as NoteStyleOptions } from '@joplin/renderer/noteStyle';
|
import { Options as NoteStyleOptions } from '@joplin/renderer/noteStyle';
|
||||||
|
import markupRenderOptions from '../../utils/markupRenderOptions';
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
const supportedLocales = require('./supportedLocales');
|
const supportedLocales = require('./supportedLocales');
|
||||||
|
|
||||||
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
|
||||||
return {
|
|
||||||
plugins: {
|
|
||||||
checkbox: {
|
|
||||||
checkboxRenderingType: 2,
|
|
||||||
},
|
|
||||||
link_open: {
|
|
||||||
linkRenderingType: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
replaceResourceInternalToExternalLinks: true,
|
|
||||||
...override,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// In TinyMCE 5.2, when setting the body to '<div id="rendered-md"></div>',
|
// In TinyMCE 5.2, when setting the body to '<div id="rendered-md"></div>',
|
||||||
// it would end up as '<div id="rendered-md"><br/></div>' once rendered
|
// it would end up as '<div id="rendered-md"><br/></div>' once rendered
|
||||||
// (an additional <br/> was inserted).
|
// (an additional <br/> was inserted).
|
||||||
@ -123,7 +108,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
|||||||
const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll });
|
const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll });
|
||||||
|
|
||||||
usePluginServiceRegistration(ref);
|
usePluginServiceRegistration(ref);
|
||||||
useContextMenu(editor, props.plugins, props.dispatch);
|
useContextMenu(editor, props.plugins, props.dispatch, props.htmlToMarkdown, props.markupToHtml);
|
||||||
|
|
||||||
const dispatchDidUpdate = (editor: any) => {
|
const dispatchDidUpdate = (editor: any) => {
|
||||||
if (dispatchDidUpdateIID_) shim.clearTimeout(dispatchDidUpdateIID_);
|
if (dispatchDidUpdateIID_) shim.clearTimeout(dispatchDidUpdateIID_);
|
||||||
@ -978,7 +963,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
|||||||
props_onChangeRef.current = props.onChange;
|
props_onChangeRef.current = props.onChange;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
const prop_htmlToMarkdownRef = useRef<Function>();
|
const prop_htmlToMarkdownRef = useRef<HtmlToMarkdownHandler>();
|
||||||
prop_htmlToMarkdownRef.current = props.htmlToMarkdown;
|
prop_htmlToMarkdownRef.current = props.htmlToMarkdown;
|
||||||
|
|
||||||
const nextOnChangeEventInfo = useRef<any>(null);
|
const nextOnChangeEventInfo = useRef<any>(null);
|
||||||
@ -1136,7 +1121,11 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
|||||||
editor.insertContent(result.html);
|
editor.insertContent(result.html);
|
||||||
} else { // Paste regular text
|
} else { // Paste regular text
|
||||||
if (pastedHtml) { // Handles HTML
|
if (pastedHtml) { // Handles HTML
|
||||||
const modifiedHtml = await processPastedHtml(pastedHtml);
|
const modifiedHtml = await processPastedHtml(
|
||||||
|
pastedHtml,
|
||||||
|
prop_htmlToMarkdownRef.current,
|
||||||
|
markupToHtml.current,
|
||||||
|
);
|
||||||
editor.insertContent(modifiedHtml);
|
editor.insertContent(modifiedHtml);
|
||||||
} else { // Handles plain text
|
} else { // Handles plain text
|
||||||
pasteAsPlainText(pastedText);
|
pasteAsPlainText(pastedText);
|
||||||
|
@ -12,6 +12,7 @@ import Setting from '@joplin/lib/models/Setting';
|
|||||||
|
|
||||||
import Resource from '@joplin/lib/models/Resource';
|
import Resource from '@joplin/lib/models/Resource';
|
||||||
import { TinyMceEditorEvents } from './types';
|
import { TinyMceEditorEvents } from './types';
|
||||||
|
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from '../../../utils/types';
|
||||||
|
|
||||||
const menuUtils = new MenuUtils(CommandService.instance());
|
const menuUtils = new MenuUtils(CommandService.instance());
|
||||||
|
|
||||||
@ -42,11 +43,11 @@ interface ContextMenuActionOptions {
|
|||||||
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
|
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
export default function(editor: any, plugins: PluginStates, dispatch: Function) {
|
export default function(editor: any, plugins: PluginStates, dispatch: Function, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return () => {};
|
if (!editor) return () => {};
|
||||||
|
|
||||||
const contextMenuItems = menuItems(dispatch);
|
const contextMenuItems = menuItems(dispatch, htmlToMd, mdToHtml);
|
||||||
|
|
||||||
function onContextMenu(_event: any, params: any) {
|
function onContextMenu(_event: any, params: any) {
|
||||||
const element = contextMenuElement(editor, params.x, params.y);
|
const element = contextMenuElement(editor, params.x, params.y);
|
||||||
@ -82,6 +83,8 @@ export default function(editor: any, plugins: PluginStates, dispatch: Function)
|
|||||||
fireEditorEvent: (event: TinyMceEditorEvents) => {
|
fireEditorEvent: (event: TinyMceEditorEvents) => {
|
||||||
editor.fire(event);
|
editor.fire(event);
|
||||||
},
|
},
|
||||||
|
htmlToMd,
|
||||||
|
mdToHtml,
|
||||||
};
|
};
|
||||||
|
|
||||||
let template = [];
|
let template = [];
|
||||||
@ -118,5 +121,5 @@ export default function(editor: any, plugins: PluginStates, dispatch: Function)
|
|||||||
bridge().window().webContents.off('context-menu', onContextMenu);
|
bridge().window().webContents.off('context-menu', onContextMenu);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [editor, plugins, dispatch]);
|
}, [editor, plugins, dispatch, htmlToMd, mdToHtml]);
|
||||||
}
|
}
|
||||||
|
@ -341,7 +341,7 @@ function NoteEditor(props: NoteEditorProps) {
|
|||||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||||
}, [formNote, handleProvisionalFlag]);
|
}, [formNote, handleProvisionalFlag]);
|
||||||
|
|
||||||
const onMessage = useMessageHandler(scrollWhenReady, setScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote);
|
const onMessage = useMessageHandler(scrollWhenReady, setScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||||
|
|
||||||
const externalEditWatcher_noteChange = useCallback((event: any) => {
|
const externalEditWatcher_noteChange = useCallback((event: any) => {
|
||||||
if (event.id === formNote.id) {
|
if (event.id === formNote.id) {
|
||||||
|
@ -14,6 +14,7 @@ import { TinyMceEditorEvents } from '../NoteBody/TinyMCE/utils/types';
|
|||||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||||
|
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { writeFile } = require('fs-extra');
|
const { writeFile } = require('fs-extra');
|
||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
@ -77,7 +78,7 @@ export async function openItemById(itemId: string, dispatch: Function, hash = ''
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
export function menuItems(dispatch: Function): ContextMenuItems {
|
export function menuItems(dispatch: Function, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler): ContextMenuItems {
|
||||||
return {
|
return {
|
||||||
open: {
|
open: {
|
||||||
label: _('Open...'),
|
label: _('Open...'),
|
||||||
@ -179,7 +180,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
|||||||
let content = pastedHtml ? pastedHtml : clipboard.readText();
|
let content = pastedHtml ? pastedHtml : clipboard.readText();
|
||||||
|
|
||||||
if (pastedHtml) {
|
if (pastedHtml) {
|
||||||
content = await processPastedHtml(pastedHtml);
|
content = await processPastedHtml(pastedHtml, htmlToMd, mdToHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
options.insertContent(content);
|
options.insertContent(content);
|
||||||
@ -207,7 +208,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
|||||||
export default async function contextMenu(options: ContextMenuOptions, dispatch: Function) {
|
export default async function contextMenu(options: ContextMenuOptions, dispatch: Function) {
|
||||||
const menu = new Menu();
|
const menu = new Menu();
|
||||||
|
|
||||||
const items = menuItems(dispatch);
|
const items = menuItems(dispatch, options.htmlToMd, options.mdToHtml);
|
||||||
|
|
||||||
if (!('readyOnly' in options)) options.isReadOnly = true;
|
if (!('readyOnly' in options)) options.isReadOnly = true;
|
||||||
for (const itemKey in items) {
|
for (const itemKey in items) {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import Resource from '@joplin/lib/models/Resource';
|
import Resource from '@joplin/lib/models/Resource';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||||
|
|
||||||
const logger = Logger.create('contextMenuUtils');
|
const logger = Logger.create('contextMenuUtils');
|
||||||
|
|
||||||
export enum ContextMenuItemType {
|
export enum ContextMenuItemType {
|
||||||
None = '',
|
None = '',
|
||||||
Image = 'image',
|
Image = 'image',
|
||||||
@ -22,6 +25,8 @@ export interface ContextMenuOptions {
|
|||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
fireEditorEvent: Function;
|
fireEditorEvent: Function;
|
||||||
|
htmlToMd: HtmlToMarkdownHandler;
|
||||||
|
mdToHtml: MarkupToHtmlHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextMenuItem {
|
export interface ContextMenuItem {
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { MarkupToHtmlOptions } from './types';
|
||||||
|
|
||||||
|
export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
|
||||||
|
return {
|
||||||
|
plugins: {
|
||||||
|
checkbox: {
|
||||||
|
checkboxRenderingType: 2,
|
||||||
|
},
|
||||||
|
link_open: {
|
||||||
|
linkRenderingType: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
replaceResourceInternalToExternalLinks: true,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
};
|
@ -1,5 +1,8 @@
|
|||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import { processPastedHtml } from './resourceHandling';
|
import { processPastedHtml } from './resourceHandling';
|
||||||
|
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
|
||||||
|
import HtmlToMd from '@joplin/lib/HtmlToMd';
|
||||||
|
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||||
|
|
||||||
describe('resourceHandling', () => {
|
describe('resourceHandling', () => {
|
||||||
it('should sanitize pasted HTML', async () => {
|
it('should sanitize pasted HTML', async () => {
|
||||||
@ -19,7 +22,32 @@ describe('resourceHandling', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const [html, expected] of testCases) {
|
for (const [html, expected] of testCases) {
|
||||||
expect(await processPastedHtml(html)).toBe(expected);
|
expect(await processPastedHtml(html, null, null)).toBe(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should clean up pasted HTML', async () => {
|
||||||
|
const markupToHtml: MarkupToHtmlHandler = async (markupLanguage, markup, options) => {
|
||||||
|
const conv = markupLanguageUtils.newMarkupToHtml({}, {
|
||||||
|
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||||
|
customCss: '',
|
||||||
|
});
|
||||||
|
return conv.render(markupLanguage, markup, {}, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlToMd: HtmlToMarkdownHandler = async (_markupLanguage, html, _originalCss) => {
|
||||||
|
const conv = new HtmlToMd();
|
||||||
|
return conv.parse(html);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
['<p style="background-color: red">Hello</p><p style="display: hidden;">World</p>', '<p>Hello</p>\n<p>World</p>\n'],
|
||||||
|
['', ''],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [html, expected] of testCases) {
|
||||||
|
expect(await processPastedHtml(html, htmlToMd, markupToHtml)).toBe(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,8 @@ import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils';
|
|||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import { fileUriToPath } from '@joplin/utils/url';
|
import { fileUriToPath } from '@joplin/utils/url';
|
||||||
import { MarkupLanguage } from '@joplin/renderer';
|
import { MarkupLanguage } from '@joplin/renderer';
|
||||||
|
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||||
|
import markupRenderOptions from './markupRenderOptions';
|
||||||
const joplinRendererUtils = require('@joplin/renderer').utils;
|
const joplinRendererUtils = require('@joplin/renderer').utils;
|
||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
|
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
|
||||||
@ -135,7 +137,7 @@ export async function getResourcesFromPasteEvent(event: any) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processPastedHtml(html: string) {
|
export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHandler | null, mdToHtml: MarkupToHtmlHandler | null) {
|
||||||
const allImageUrls: string[] = [];
|
const allImageUrls: string[] = [];
|
||||||
const mappedResources: Record<string, string> = {};
|
const mappedResources: Record<string, string> = {};
|
||||||
|
|
||||||
@ -179,6 +181,15 @@ export async function processPastedHtml(html: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TinyMCE can accept any type of HTML, including HTML that may not be preserved once saved as
|
||||||
|
// Markdown. For example the content may have a dark background which would be supported by
|
||||||
|
// TinyMCE, but lost once the note is saved. So here we convert the HTML to Markdown then back
|
||||||
|
// to HTML to ensure that the content we paste will be handled correctly by the app.
|
||||||
|
if (htmlToMd && mdToHtml) {
|
||||||
|
const md = await htmlToMd(MarkupLanguage.Markdown, html, '');
|
||||||
|
html = (await mdToHtml(MarkupLanguage.Markdown, md, markupRenderOptions({ bodyOnly: true }))).html;
|
||||||
|
}
|
||||||
|
|
||||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(
|
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(
|
||||||
htmlUtils.replaceImageUrls(html, (src: string) => {
|
htmlUtils.replaceImageUrls(html, (src: string) => {
|
||||||
return mappedResources[src];
|
return mappedResources[src];
|
||||||
|
@ -3,7 +3,6 @@ import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUt
|
|||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
import { MarkupLanguage } from '@joplin/renderer';
|
import { MarkupLanguage } from '@joplin/renderer';
|
||||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
|
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||||
import { MarkupToHtmlOptions } from './useMarkupToHtml';
|
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';
|
import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';
|
||||||
|
|
||||||
@ -61,6 +60,24 @@ export interface NoteBodyEditorRef {
|
|||||||
execCommand(command: CommandValue): Promise<void>;
|
execCommand(command: CommandValue): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MarkupToHtmlOptions {
|
||||||
|
replaceResourceInternalToExternalLinks?: boolean;
|
||||||
|
resourceInfos?: ResourceInfos;
|
||||||
|
contentMaxWidth?: number;
|
||||||
|
plugins?: Record<string, any>;
|
||||||
|
bodyOnly?: boolean;
|
||||||
|
mapsToLine?: boolean;
|
||||||
|
useCustomPdfViewer?: boolean;
|
||||||
|
noteId?: string;
|
||||||
|
vendorDir?: string;
|
||||||
|
platformName?: string;
|
||||||
|
allowedFilePrefixes?: string[];
|
||||||
|
whiteBackgroundNoteRendering?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarkupToHtmlHandler = (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||||
|
export type HtmlToMarkdownHandler = (markupLanguage: number, html: string, originalCss: string)=> Promise<string>;
|
||||||
|
|
||||||
export interface NoteBodyEditorProps {
|
export interface NoteBodyEditorProps {
|
||||||
style: any;
|
style: any;
|
||||||
ref: any;
|
ref: any;
|
||||||
@ -81,9 +98,8 @@ export interface NoteBodyEditorProps {
|
|||||||
onWillChange(event: any): void;
|
onWillChange(event: any): void;
|
||||||
onMessage(event: any): void;
|
onMessage(event: any): void;
|
||||||
onScroll(event: { percent: number }): void;
|
onScroll(event: { percent: number }): void;
|
||||||
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
markupToHtml: MarkupToHtmlHandler;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
htmlToMarkdown: HtmlToMarkdownHandler;
|
||||||
htmlToMarkdown: Function;
|
|
||||||
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
|
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { ResourceInfos } from './types';
|
|
||||||
import markupLanguageUtils from '../../../utils/markupLanguageUtils';
|
import markupLanguageUtils from '../../../utils/markupLanguageUtils';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
|
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
const { themeStyle } = require('@joplin/lib/theme');
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
|
import { MarkupToHtmlOptions } from './types';
|
||||||
|
|
||||||
interface HookDependencies {
|
interface HookDependencies {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
@ -16,21 +16,6 @@ interface HookDependencies {
|
|||||||
whiteBackgroundNoteRendering: boolean;
|
whiteBackgroundNoteRendering: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkupToHtmlOptions {
|
|
||||||
replaceResourceInternalToExternalLinks?: boolean;
|
|
||||||
resourceInfos?: ResourceInfos;
|
|
||||||
contentMaxWidth?: number;
|
|
||||||
plugins?: Record<string, any>;
|
|
||||||
bodyOnly?: boolean;
|
|
||||||
mapsToLine?: boolean;
|
|
||||||
useCustomPdfViewer?: boolean;
|
|
||||||
noteId?: string;
|
|
||||||
vendorDir?: string;
|
|
||||||
platformName?: string;
|
|
||||||
allowedFilePrefixes?: string[];
|
|
||||||
whiteBackgroundNoteRendering?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useMarkupToHtml(deps: HookDependencies) {
|
export default function useMarkupToHtml(deps: HookDependencies) {
|
||||||
const { themeId, customCss, plugins, whiteBackgroundNoteRendering } = deps;
|
const { themeId, customCss, plugins, whiteBackgroundNoteRendering } = deps;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { FormNote } from './types';
|
import { FormNote, HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||||
import contextMenu from './contextMenu';
|
import contextMenu from './contextMenu';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
import PostMessageService from '@joplin/lib/services/PostMessageService';
|
import PostMessageService from '@joplin/lib/services/PostMessageService';
|
||||||
@ -8,7 +8,7 @@ import { reg } from '@joplin/lib/registry';
|
|||||||
const bridge = require('@electron/remote').require('./bridge').default;
|
const bridge = require('@electron/remote').require('./bridge').default;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
export default function useMessageHandler(scrollWhenReady: any, setScrollWhenReady: Function, editorRef: any, setLocalSearchResultCount: Function, dispatch: Function, formNote: FormNote) {
|
export default function useMessageHandler(scrollWhenReady: any, setScrollWhenReady: Function, editorRef: any, setLocalSearchResultCount: Function, dispatch: Function, formNote: FormNote, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) {
|
||||||
return useCallback(async (event: any) => {
|
return useCallback(async (event: any) => {
|
||||||
const msg = event.channel ? event.channel : '';
|
const msg = event.channel ? event.channel : '';
|
||||||
const args = event.args;
|
const args = event.args;
|
||||||
@ -44,6 +44,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
|||||||
htmlToCopy: '',
|
htmlToCopy: '',
|
||||||
insertContent: () => { console.warn('insertContent() not implemented'); },
|
insertContent: () => { console.warn('insertContent() not implemented'); },
|
||||||
fireEditorEvent: () => { console.warn('fireEditorEvent() not implemented'); },
|
fireEditorEvent: () => { console.warn('fireEditorEvent() not implemented'); },
|
||||||
|
htmlToMd,
|
||||||
|
mdToHtml,
|
||||||
}, dispatch);
|
}, dispatch);
|
||||||
|
|
||||||
menu.popup({ window: bridge().window() });
|
menu.popup({ window: bridge().window() });
|
||||||
|
@ -61,6 +61,8 @@ export default function PdfViewer(props: Props) {
|
|||||||
htmlToCopy: '',
|
htmlToCopy: '',
|
||||||
insertContent: () => { console.warn('insertContent() not implemented'); },
|
insertContent: () => { console.warn('insertContent() not implemented'); },
|
||||||
fireEditorEvent: () => { console.warn('fireEditorEvent() not implemented'); },
|
fireEditorEvent: () => { console.warn('fireEditorEvent() not implemented'); },
|
||||||
|
htmlToMd: async (_a, b, _c) => b,
|
||||||
|
mdToHtml: async (_a, b, _c) => { return { html: b, pluginAssets: [], cssStrings: [] }; },
|
||||||
} as ContextMenuOptions, props.dispatch);
|
} as ContextMenuOptions, props.dispatch);
|
||||||
|
|
||||||
menu.popup({ window: bridge().window() });
|
menu.popup({ window: bridge().window() });
|
||||||
|
Loading…
Reference in New Issue
Block a user