support paste page html

This commit is contained in:
fantasticit 2022-12-18 12:25:04 +08:00
parent 8453788b84
commit edadc508e7
3 changed files with 112 additions and 109 deletions

View File

@ -1,52 +1,79 @@
import { Editor as CoreEditor, Extension } from '@tiptap/core';
import { Editor as CoreEditor, Extension, getSchema } from '@tiptap/core';
import { Document } from '@tiptap/extension-document';
import { safeJSONParse } from 'helpers/json';
import { toggleMark } from 'prosemirror-commands';
import { DOMParser, Fragment, Node, Schema } from 'prosemirror-model';
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
import { DOMParser as PMDOMParser, Fragment, Node, Schema } from 'prosemirror-model';
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
import {
debug,
fixHTML,
handleFileEvent,
isInCode,
isInTitle,
isMarkdown,
isTitleNode,
isValidURL,
normalizeMarkdown,
} from 'tiptap/prose-utils';
import { TitleExtensionName } from './title';
const safePos = (state: EditorState, pos) => {
if (pos < 0) return 0;
function insertText(view, text) {
const texts = text.split('\n').filter(Boolean);
event.preventDefault();
view.dispatch(view.state.tr.insertText(texts[0]));
return Math.min(state.doc.content.size, pos);
};
const json = {
const htmlToProsemirror = (editor: CoreEditor, html, isPasteMarkdown = false) => {
const firstNode = editor.view.state.doc.content.firstChild;
const shouldInsertTitleText = !!(firstNode?.textContent?.length <= 0 ?? true);
if (!shouldInsertTitleText && !isPasteMarkdown) return false;
const parser = new window.DOMParser();
const { body } = parser.parseFromString(fixHTML(html), 'text/html');
const schema = getSchema(
[].concat(
Document,
editor.extensionManager.extensions.filter(
(ext) => ext.type === 'node' && !['title', 'doc', 'collaboration', 'collaborationCursor'].includes(ext.name)
)
)
);
const toPasteNode = PMDOMParser.fromSchema(schema).parse(body);
const doc = {
type: 'doc',
content: [{ type: 'title', attrs: { cover: '' }, content: [{ type: 'text', text: texts[0] }] }].concat(
// @ts-ignore
texts.slice(1).map((t) => {
return {
type: 'paragraph',
attrs: { indent: 0, textAlign: 'left' },
content: [{ type: 'text', text: t }],
};
})
),
content: toPasteNode.content.toJSON(),
};
let tr = view.state.tr;
const selection = tr.selection;
view.state.doc.nodesBetween(selection.from, selection.to, (node, position) => {
const startPosition = Math.min(position, selection.from) || 0;
const endPosition = Math.min(position + node.nodeSize, selection.to);
tr = tr.replaceWith(startPosition, endPosition, view.state.schema.nodeFromJSON(json));
});
view.dispatch(tr.scrollIntoView());
let toInsertAtTitleNode = null;
if (shouldInsertTitleText) {
toInsertAtTitleNode = doc.content.shift();
}
let tr = editor.view.state.tr;
const insertAt = isInTitle(editor.state)
? safePos(editor.state, firstNode.nodeSize)
: safePos(editor.state, editor.state.selection.from - 1);
const node = editor.state.schema.nodeFromJSON(doc);
tr = tr.insert(insertAt, node);
if (shouldInsertTitleText) {
if (toInsertAtTitleNode) {
if (['heading', 'paragraph'].includes(toInsertAtTitleNode.type)) {
tr.insertText(toInsertAtTitleNode?.content?.[0]?.text, 1, 1);
} else {
tr.insert(insertAt, editor.state.schema.nodeFromJSON(toInsertAtTitleNode));
}
}
}
editor.view.dispatch(tr.scrollIntoView());
return true;
}
};
interface IPasteOptions {
/**
@ -116,7 +143,6 @@ export const Paste = Extension.create<IPasteOptions>({
const node = event.clipboardData.getData('text/node');
const markdownText = event.clipboardData.getData('text/markdown');
const { state, dispatch } = view;
const { htmlToProsemirror, markdownToProsemirror } = extensionThis.options;
debug(() => {
console.group('paste');
@ -134,53 +160,6 @@ export const Paste = Extension.create<IPasteOptions>({
return true;
}
const firstNode = view.props.state.doc.content.firstChild;
const hasTitleExtension = !!editor.extensionManager.extensions.find(
(extension) => extension.name === TitleExtensionName
);
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
// If the HTML on the clipboard is from Prosemirror then the best
// compatability is to just use the HTML parser, regardless of
// whether it "looks" like Markdown, see: outline/outline#2416
if (html?.includes('data-pm-slice')) {
let domNode = document.createElement('div');
domNode.innerHTML = html;
const slice = DOMParser.fromSchema(editor.schema).parseSlice(domNode);
let tr = view.state.tr;
tr = tr.replaceSelection(slice);
view.dispatch(tr.scrollIntoView());
domNode = null;
return true;
}
// TODO各家 office 套件标准不一样,是否需要做成用户自行选择粘贴 html 或者 图片?
if (html?.includes('urn:schemas-microsoft-com:office') || html?.includes('</table>')) {
const doc = htmlToProsemirror({
editor,
schema: editor.schema,
html,
needTitle: hasTitleExtension && !hasTitle,
});
let tr = view.state.tr;
const selection = tr.selection;
view.state.doc.nodesBetween(selection.from, selection.to, (node, position) => {
const startPosition = hasTitle ? Math.min(position, selection.from) : 0;
const endPosition = Math.min(position + node.nodeSize, selection.to);
tr = tr.replaceWith(startPosition, endPosition, view.state.schema.nodeFromJSON(doc));
});
view.dispatch(tr.scrollIntoView());
return true;
}
if (files.length) {
event.preventDefault();
files.forEach((file) => {
handleFileEvent({ editor, file });
});
return true;
}
// 链接
if (isValidURL(text)) {
if (!state.selection.empty) {
@ -209,43 +188,24 @@ export const Paste = Extension.create<IPasteOptions>({
const vscodeMeta = vscode ? JSON.parse(vscode) : undefined;
const pasteCodeLanguage = vscodeMeta?.mode;
if (pasteCodeLanguage && pasteCodeLanguage !== 'markdown') {
event.preventDefault();
const { tr } = view.state;
tr.replaceSelectionWith(view.state.schema.nodes.codeBlock.create({ language: pasteCodeLanguage }));
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(0, tr.selection.from - 1))));
tr.insertText(text.replace(/\r\n?/g, '\n'));
tr.setMeta('paste', true);
view.dispatch(tr);
return true;
if (html.length > 0 || text.length === 0) {
return htmlToProsemirror(editor, html);
}
// 处理 markdown
if (markdownText || isMarkdown(text)) {
console.log(text);
const { markdownToHTML } = extensionThis.options;
if ((markdownText || isMarkdown(text)) && markdownToHTML) {
event.preventDefault();
const schema = view.props.state.schema;
const doc = markdownToProsemirror({
editor,
schema,
content: normalizeMarkdown(markdownText || text),
needTitle: hasTitleExtension && !hasTitle,
});
let tr = view.state.tr;
const selection = tr.selection;
view.state.doc.nodesBetween(selection.from, selection.to, (node, position) => {
const startPosition = hasTitle ? Math.min(position, selection.from) : 0;
const endPosition = Math.min(position + node.nodeSize, selection.to);
tr = tr.replaceWith(startPosition, endPosition, view.state.schema.nodeFromJSON(doc));
});
view.dispatch(tr.scrollIntoView());
return true;
const html = markdownToHTML(normalizeMarkdown(markdownText || text));
if (html && html.length) return htmlToProsemirror(editor, html, true);
}
if (isInTitle(view.state)) {
if (text.length) {
return insertText(view, text);
}
if (files.length) {
event.preventDefault();
files.forEach((file) => {
handleFileEvent({ editor, file });
});
return true;
}
return false;

View File

@ -0,0 +1,42 @@
export function fixHTML(html) {
const container = document.createElement('div');
container.innerHTML = html;
let el;
while ((el = container.querySelector('a > img'))) {
unwrapLink(el.parentNode, el.getAttribute('alt') || 'Image link');
}
while ((el = container.querySelector('p > img'))) {
unwrap(el.parentNode);
}
while ((el = container.querySelector('a:not(p a)'))) {
wrap(el, document.createElement('p'));
}
return container.innerHTML;
}
function unwrap(el) {
const parent = el.parentNode;
// Move all children to the parent element.
while (el.firstChild) parent.insertBefore(el.firstChild, el);
parent.removeChild(el);
}
function unwrapLink(el, replacementText) {
const parent = el.parentNode;
while (el.firstChild) parent.insertBefore(el.firstChild, el);
el.textContent = replacementText;
}
function wrap(el, wrapper) {
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
}

View File

@ -13,6 +13,7 @@ export * from './dom-dataset';
export * from './download';
export * from './editor-container-size';
export * from './file';
export * from './html';
export * from './lowlight-plugin';
export * from './mark';
export * from './markdown';