editor: refactor clipboard extension

This commit is contained in:
Abdullah Atta
2023-08-26 19:24:29 +05:00
committed by Abdullah Atta
parent 0cfa7ce774
commit a3d3886c51
17 changed files with 351 additions and 267 deletions

View File

@@ -38,7 +38,6 @@
"@tiptap/extension-underline": "2.0.3", "@tiptap/extension-underline": "2.0.3",
"@tiptap/pm": "2.0.3", "@tiptap/pm": "2.0.3",
"@tiptap/starter-kit": "2.0.3", "@tiptap/starter-kit": "2.0.3",
"clipboard-polyfill": "4.0.0",
"detect-indent": "^7.0.0", "detect-indent": "^7.0.0",
"entities": "^4.5.0", "entities": "^4.5.0",
"katex": "0.16.4", "katex": "0.16.4",
@@ -1910,11 +1909,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/clipboard-polyfill": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/clipboard-polyfill/-/clipboard-polyfill-4.0.0.tgz",
"integrity": "sha512-U4KPNJqAYuyOtixCZZUyWTcj+wlI66j07g5ggMRE2DR1VFu/3ZWXkjLAslmme8i065gBSCUblHET7DKQ2Xg3RA=="
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -5098,11 +5092,6 @@
} }
} }
}, },
"clipboard-polyfill": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/clipboard-polyfill/-/clipboard-polyfill-4.0.0.tgz",
"integrity": "sha512-U4KPNJqAYuyOtixCZZUyWTcj+wlI66j07g5ggMRE2DR1VFu/3ZWXkjLAslmme8i065gBSCUblHET7DKQ2Xg3RA=="
},
"color-convert": { "color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",

View File

@@ -33,7 +33,6 @@
"@tiptap/extension-underline": "2.0.3", "@tiptap/extension-underline": "2.0.3",
"@tiptap/pm": "2.0.3", "@tiptap/pm": "2.0.3",
"@tiptap/starter-kit": "2.0.3", "@tiptap/starter-kit": "2.0.3",
"clipboard-polyfill": "4.0.0",
"detect-indent": "^7.0.0", "detect-indent": "^7.0.0",
"entities": "^4.5.0", "entities": "^4.5.0",
"katex": "0.16.4", "katex": "0.16.4",

View File

@@ -1,152 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Extension, TextSerializer } from "@tiptap/core";
import { Plugin, PluginKey } from "prosemirror-state";
import { Fragment, Schema, Slice } from "prosemirror-model";
import { ListItem } from "../list-item";
import { LIST_NODE_TYPES } from "../../utils/node-types";
import {
DOMSerializer,
DOMParser as ProsemirrorDOMParser
} from "@tiptap/pm/model";
import { convertTextToHTML } from "../../utils/html";
export class ClipboardDOMSerializer extends DOMSerializer {
static fromSchema(schema: Schema): ClipboardDOMSerializer {
return (
schema.cached.domSerializer2 ||
(schema.cached.domSerializer2 = new ClipboardDOMSerializer(
this.nodesFromSchema(schema),
this.marksFromSchema(schema)
))
);
}
serializeFragment(
fragment: Fragment,
options?: { document?: Document | undefined } | undefined,
target?: HTMLElement | DocumentFragment | undefined
): HTMLElement | DocumentFragment {
const dom = super.serializeFragment(fragment, options, target);
for (const p of dom.querySelectorAll("li > p")) {
if (p.parentElement && p.parentElement.childElementCount > 1) continue;
p.parentElement?.append(...p.childNodes);
p.remove();
}
for (const p of dom.querySelectorAll('p[data-spacing="single"]')) {
if (!p.previousElementSibling || p.previousElementSibling.tagName !== "P")
continue;
if (p.previousElementSibling.childNodes.length > 0)
p.previousElementSibling.appendChild(document.createElement("br"));
p.previousElementSibling.append(...p.childNodes);
p.remove();
}
return dom;
}
}
export const ClipboardTextSerializer = Extension.create({
name: "clipboardTextSerializer",
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("clipboardTextSerializer"),
props: {
transformCopied,
clipboardSerializer: ClipboardDOMSerializer.fromSchema(
this.editor.view.state.schema
),
clipboardTextParser: (text) => {
const doc = new DOMParser().parseFromString(
convertTextToHTML(text),
"text/html"
);
return ProsemirrorDOMParser.fromSchema(
this.editor.view.state.schema
).parseSlice(doc, { preserveWhitespace: "full" });
},
clipboardTextSerializer: (content, view) => {
return getTextBetween(content, view.state.schema);
}
}
})
];
}
});
export function transformCopied(slice: Slice) {
// when copying a single list item, we shouldn't retain the
// list formatting but copy it as a paragraph.
const maybeList = slice.content.firstChild;
if (
maybeList &&
LIST_NODE_TYPES.includes(maybeList.type.name) &&
maybeList.childCount === 1 &&
maybeList.firstChild
) {
return transformCopied(new Slice(maybeList.firstChild.content, 0, 0));
}
return slice;
}
export function getTextBetween(slice: Slice, schema: Schema): string {
const range = { from: 0, to: slice.size };
const separator = "\n";
let text = "";
let separated = true;
slice.content.nodesBetween(0, slice.size, (node, pos, parent, index) => {
const textSerializer = schema.nodes[node.type.name]?.spec
.toText as TextSerializer;
if (textSerializer) {
if (node.isBlock && !separated) {
text += separator;
separated = true;
}
if (parent) {
text += textSerializer({
node,
pos,
parent,
index,
range
});
}
} else if (node.isText) {
text += node?.text;
separated = false;
} else if (node.isBlock && !!text) {
// we don't want double spaced list items when pasting
if (index === 0 && parent?.type.name === ListItem.name) return;
text += separator;
if (node.attrs.spacing === "double" && node.childCount > 0)
text += separator;
separated = true;
}
});
return text;
}

View File

@@ -17,11 +17,32 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { encodeNonAsciiHTML } from "entities"; import {
DOMParser as ProsemirrorDOMParser,
ParseOptions
} from "@tiptap/pm/model";
import { Schema, Slice } from "prosemirror-model";
export function convertBrToParagraph(html: string) { export class ClipboardDOMParser extends ProsemirrorDOMParser {
const doc = new DOMParser().parseFromString(html, "text/html"); static fromSchema(schema: Schema): ClipboardDOMParser {
for (const br of doc.querySelectorAll("br")) { return (
(schema.cached.clipboardDomParser as ClipboardDOMParser) ||
(schema.cached.clipboardDomParser = new ClipboardDOMParser(
schema,
(ProsemirrorDOMParser as any).schemaRules(schema)
))
);
}
parseSlice(dom: Node, options?: ParseOptions | undefined): Slice {
convertBrToSingleSpacedParagraphs(dom);
return super.parseSlice(dom, options);
}
}
export function convertBrToSingleSpacedParagraphs(dom: Node) {
if (!(dom instanceof HTMLElement)) return;
for (const br of dom.querySelectorAll("br")) {
let paragraph = br.closest("p"); let paragraph = br.closest("p");
// if no paragraph is found over the br, we add one. // if no paragraph is found over the br, we add one.
@@ -37,14 +58,13 @@ export function convertBrToParagraph(html: string) {
if (paragraph) { if (paragraph) {
splitOn(paragraph, br); splitOn(paragraph, br);
const children = Array.from(paragraph.childNodes.values()); const children = Array.from(paragraph.childNodes.values());
const newParagraph = doc.createElement("p"); const newParagraph = document.createElement("p");
newParagraph.dataset.spacing = "single"; newParagraph.dataset.spacing = "single";
newParagraph.append(...children.slice(children.indexOf(br) + 1)); newParagraph.append(...children.slice(children.indexOf(br) + 1));
paragraph.insertAdjacentElement("afterend", newParagraph); paragraph.insertAdjacentElement("afterend", newParagraph);
br.remove(); br.remove();
} }
} }
return doc;
} }
function splitOn(bound: Element, cutElement: Element) { function splitOn(bound: Element, cutElement: Element) {
@@ -63,25 +83,3 @@ function splitOn(bound: Element, cutElement: Element) {
} }
} }
} }
export function convertTextToHTML(src: string) {
return src
.split(/[\r\n]/)
.map((line) =>
line
? `<p data-spacing="single">${encodeLine(line)}</p>`
: `<p data-spacing="single"></p>`
)
.join("");
}
function encodeLine(line: string) {
line = encodeNonAsciiHTML(line);
line = line.replace(/(^ +)|( {2,})/g, (sub, ...args) => {
const [starting, inline] = args;
if (starting) return "&nbsp;".repeat(starting.length);
if (inline) return "&nbsp;".repeat(inline.length);
return sub;
});
return line;
}

View File

@@ -0,0 +1,57 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Fragment, Schema } from "prosemirror-model";
import { DOMSerializer } from "@tiptap/pm/model";
export class ClipboardDOMSerializer extends DOMSerializer {
static fromSchema(schema: Schema): ClipboardDOMSerializer {
return (
schema.cached.clipboardDomSerializer ||
(schema.cached.clipboardDomSerializer = new ClipboardDOMSerializer(
this.nodesFromSchema(schema),
this.marksFromSchema(schema)
))
);
}
serializeFragment(
fragment: Fragment,
options?: { document?: Document | undefined } | undefined,
target?: HTMLElement | DocumentFragment | undefined
): HTMLElement | DocumentFragment {
const dom = super.serializeFragment(fragment, options, target);
for (const p of dom.querySelectorAll("li > p")) {
if (p.parentElement && p.parentElement.childElementCount > 1) continue;
p.parentElement?.append(...p.childNodes);
p.remove();
}
for (const p of dom.querySelectorAll('p[data-spacing="single"]')) {
if (!p.previousElementSibling || p.previousElementSibling.tagName !== "P")
continue;
if (p.previousElementSibling.childNodes.length > 0)
p.previousElementSibling.appendChild(document.createElement("br"));
p.previousElementSibling.append(...p.childNodes);
p.remove();
}
return dom;
}
}

View File

@@ -0,0 +1,60 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ResolvedPos, Slice } from "@tiptap/pm/model";
import { encodeNonAsciiHTML } from "entities";
import { ClipboardDOMParser } from "./clipboard-dom-parser";
import { EditorView } from "@tiptap/pm/view";
export function clipboardTextParser(
text: string,
_$context: ResolvedPos,
_plain: boolean,
view: EditorView
): Slice {
const doc = new DOMParser().parseFromString(
convertTextToHTML(text),
"text/html"
);
return ClipboardDOMParser.fromSchema(view.state.schema).parseSlice(doc, {
preserveWhitespace: "full"
});
}
export function convertTextToHTML(src: string) {
return src
.split(/[\r\n]/)
.map((line) =>
line
? `<p data-spacing="single">${encodeLine(line)}</p>`
: `<p data-spacing="single"></p>`
)
.join("");
}
function encodeLine(line: string) {
line = encodeNonAsciiHTML(line);
line = line.replace(/(^ +)|( {2,})/g, (sub, ...args) => {
const [starting, inline] = args;
if (starting) return "&nbsp;".repeat(starting.length);
if (inline) return "&nbsp;".repeat(inline.length);
return sub;
});
return line;
}

View File

@@ -0,0 +1,69 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { TextSerializer } from "@tiptap/core";
import { Schema, Slice } from "prosemirror-model";
import { ListItem } from "../list-item";
import { EditorView } from "@tiptap/pm/view";
export function clipboardTextSerializer(content: Slice, view: EditorView) {
return getTextBetween(content, view.state.schema);
}
export function getTextBetween(slice: Slice, schema: Schema): string {
const range = { from: 0, to: slice.size };
const separator = "\n";
let text = "";
let separated = true;
slice.content.nodesBetween(0, slice.size, (node, pos, parent, index) => {
const textSerializer = schema.nodes[node.type.name]?.spec
.toText as TextSerializer;
if (textSerializer) {
if (node.isBlock && !separated) {
text += separator;
separated = true;
}
if (parent) {
text += textSerializer({
node,
pos,
parent,
index,
range
});
}
} else if (node.isText) {
text += node?.text;
separated = false;
} else if (node.isBlock && !!text) {
// we don't want double spaced list items when pasting
if (index === 0 && parent?.type.name === ListItem.name) return;
text += separator;
if (node.attrs.spacing === "double" && node.childCount > 0)
text += separator;
separated = true;
}
});
return text;
}

View File

@@ -16,8 +16,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Extension } from "@tiptap/core"; import { Extension } from "@tiptap/core";
import { writeText } from "clipboard-polyfill"; import { Plugin, PluginKey } from "prosemirror-state";
import { Slice } from "prosemirror-model";
import { LIST_NODE_TYPES } from "../../utils/node-types";
import { ClipboardDOMParser } from "./clipboard-dom-parser";
import { ClipboardDOMSerializer } from "./clipboard-dom-serializer";
import { clipboardTextParser } from "./clipboard-text-parser";
import { clipboardTextSerializer } from "./clipboard-text-serializer";
declare module "@tiptap/core" { declare module "@tiptap/core" {
interface Commands<ReturnType> { interface Commands<ReturnType> {
@@ -31,14 +38,15 @@ export type ClipboardOptions = {
copyToClipboard: (text: string) => void; copyToClipboard: (text: string) => void;
}; };
export const Clipboard = Extension.create<ClipboardOptions>({ export const Clipboard = Extension.create({
name: "clipboard",
addOptions() { addOptions() {
return { return {
copyToClipboard: (text) => { copyToClipboard: () => {}
writeText(text);
}
}; };
}, },
addCommands() { addCommands() {
return { return {
copyToClipboard: (text: string) => (props) => { copyToClipboard: (text: string) => (props) => {
@@ -46,5 +54,39 @@ export const Clipboard = Extension.create<ClipboardOptions>({
return true; return true;
} }
}; };
},
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("clipboard"),
props: {
clipboardParser: ClipboardDOMParser.fromSchema(
this.editor.view.state.schema
),
clipboardSerializer: ClipboardDOMSerializer.fromSchema(
this.editor.view.state.schema
),
transformCopied,
clipboardTextParser,
clipboardTextSerializer
}
})
];
} }
}); });
export function transformCopied(slice: Slice) {
// when copying a single list item, we shouldn't retain the
// list formatting but copy it as a paragraph.
const maybeList = slice.content.firstChild;
if (
maybeList &&
LIST_NODE_TYPES.includes(maybeList.type.name) &&
maybeList.childCount === 1 &&
maybeList.firstChild
) {
return transformCopied(new Slice(maybeList.firstChild.content, 0, 0));
}
return slice;
}

View File

@@ -16,8 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Clipboard } from "./clipboard";
export * from "./clipboard"; export * from "./clipboard";
export { Clipboard as default } from "./clipboard";
export default Clipboard;

View File

@@ -0,0 +1,69 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`convert br tags to paragraphs (
<html><body>
<!--StartFragment-->A troll, they call me, but I have no wish<br>
to be associated with those dolls<br>
<br>
We lack religion, purpose, politics,<br>
and yet, we somehow manage to get by.<br>
</body>
</html>) 1`] = `
"<!--StartFragment-->A troll, they call me, but I have no wish<br>
to be associated with those dolls<br>
<br>
We lack religion, purpose, politics,<br>
and yet, we somehow manage to get by.<br>"
`;
exports[`convert br tags to paragraphs (<html><body>
<!--StartFragment--><p dir="auto">When I try to paste something (e.g. email content) to a note, the styling is kept, which is good, but the newlines are removed.<br>
Also when I share the selection to Notesnook via the share functionality from Android, I have the same issue.</p>
<hr>
<p dir="auto"><strong>Device information:</strong><br>
App version: 2.3.0<br>
Platform: android<br>
Model: OnePlus-CPH2409-31<br>
Pro: true<br>
Logged in: yes</p><!--EndFragment-->
</body>
</html>) 1`] = `
"<!--StartFragment--><p dir=\\"auto\\">When I try to paste something (e.g. email content) to a note, the styling is kept, which is good, but the newlines are removed.<br>
Also when I share the selection to Notesnook via the share functionality from Android, I have the same issue.</p>
<hr>
<p dir=\\"auto\\"><strong>Device information:</strong><br>
App version: 2.3.0<br>
Platform: android<br>
Model: OnePlus-CPH2409-31<br>
Pro: true<br>
Logged in: yes</p><!--EndFragment-->"
`;
exports[`convert br tags to paragraphs (<html><body>
<!--StartFragment--><span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">Why switch from Gmail?
Not sacrificing features for more privacy, prefer using one app, in many public groups and channels (Telegram)
LibreOffice Slow &amp; buggy
Switched to Brave for the better Android app, more private out of the box &amp; unsure if uBlock Origin closes gap</span><!--EndFragment-->
</body>
</html>) 1`] = `
"<!--StartFragment--><span class=\\"css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0\\">Why switch from Gmail?
Not sacrificing features for more privacy, prefer using one app, in many public groups and channels (Telegram)
LibreOffice Slow &amp; buggy
Switched to Brave for the better Android app, more private out of the box &amp; unsure if uBlock Origin closes gap</span><!--EndFragment-->"
`;
exports[`convert br tags to paragraphs (<p><br/></p>) 1`] = `"<p><br></p>"`;
exports[`convert br tags to paragraphs (<p>line <em>1<br>line</em> 2</p>) 1`] = `"<p>line <em>1<br>line</em> 2</p>"`;
exports[`convert br tags to paragraphs (<p>line <span><em>1<br data-some="hello">line</em></span> 2</p>) 1`] = `"<p>line <span><em>1<br data-some=\\"hello\\">line</em></span> 2</p>"`;
exports[`convert br tags to paragraphs (<p>line <span><em>1<br>line</em></span> 2</p>) 1`] = `"<p>line <span><em>1<br>line</em></span> 2</p>"`;
exports[`convert br tags to paragraphs (<p>line 1<br>line 2</p>) 1`] = `"<p>line 1<br>line 2</p>"`;

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { test } from "vitest"; import { test } from "vitest";
import { convertBrToParagraph } from "../html"; import { convertBrToSingleSpacedParagraphs } from "../clipboard-dom-parser";
const cases = [ const cases = [
[`<p>line 1<br>line 2</p>`], [`<p>line 1<br>line 2</p>`],
@@ -68,8 +68,8 @@ and yet, we somehow manage to get by.<br>
for (const testCase of cases) { for (const testCase of cases) {
const [html, expected] = testCase; const [html, expected] = testCase;
test(`convert br tags to paragraphs (${testCase})`, (t) => { test(`convert br tags to paragraphs (${testCase})`, (t) => {
t.expect( const element = new DOMParser().parseFromString(html, "text/html");
convertBrToParagraph(html).body.innerHTML.trim() convertBrToSingleSpacedParagraphs(element);
).toMatchSnapshot(); t.expect(element.body.innerHTML.trim()).toMatchSnapshot();
}); });
} }

View File

@@ -21,12 +21,10 @@ import { test } from "vitest";
import { createEditor, h } from "../../../../test-utils"; import { createEditor, h } from "../../../../test-utils";
import OrderedList from "../../ordered-list"; import OrderedList from "../../ordered-list";
import { ListItem } from "../../list-item"; import { ListItem } from "../../list-item";
import { import { transformCopied } from "../index";
getTextBetween,
transformCopied,
ClipboardDOMSerializer
} from "../index";
import { Paragraph } from "../../paragraph"; import { Paragraph } from "../../paragraph";
import { ClipboardDOMSerializer } from "../clipboard-dom-serializer";
import { clipboardTextSerializer } from "../clipboard-text-serializer";
test("copied list items shouldn't contain extra newlines", (t) => { test("copied list items shouldn't contain extra newlines", (t) => {
const { editor } = createEditor({ const { editor } = createEditor({
@@ -59,9 +57,9 @@ test("copied list items shouldn't contain extra newlines", (t) => {
); );
t.expect( t.expect(
getTextBetween( clipboardTextSerializer(
editor.state.doc.slice(0, editor.state.doc.nodeSize - 2), editor.state.doc.slice(0, editor.state.doc.nodeSize - 2),
editor.schema editor.view
) )
).toBe(`This is line: number 1. ).toBe(`This is line: number 1.
And this is line number 2. And this is line number 2.
@@ -194,9 +192,9 @@ for (const testCase of paragraphTestCases) {
).toBe(testCase.expectedHtml); ).toBe(testCase.expectedHtml);
t.expect( t.expect(
getTextBetween( clipboardTextSerializer(
editor.state.doc.slice(0, editor.state.doc.nodeSize - 2), editor.state.doc.slice(0, editor.state.doc.nodeSize - 2),
editor.schema editor.view
) )
).toBe(testCase.expectedText); ).toBe(testCase.expectedText);
}); });

File diff suppressed because one or more lines are too long

View File

@@ -122,7 +122,10 @@ test("pasting code from vscode should automatically create a syntax highlighted
(clipboardEvent as unknown as any)["clipboardData"] = { (clipboardEvent as unknown as any)["clipboardData"] = {
getData: (type: string) => getData: (type: string) =>
type === "text/plain" type === "text/plain"
? "function hello() { }" ? `function hello()
{
const world = "hello";
}`
: type === "vscode-editor-data" : type === "vscode-editor-data"
? JSON.stringify({ mode: "javascript" }) ? JSON.stringify({ mode: "javascript" })
: undefined : undefined

View File

@@ -40,7 +40,6 @@ import { useEffect, useMemo } from "react";
import "./extensions"; import "./extensions";
import { AttachmentNode, AttachmentOptions } from "./extensions/attachment"; import { AttachmentNode, AttachmentOptions } from "./extensions/attachment";
import BulletList from "./extensions/bullet-list"; import BulletList from "./extensions/bullet-list";
import { ClipboardTextSerializer } from "./extensions/clipboard-text-serializer";
import { CodeBlock } from "./extensions/code-block"; import { CodeBlock } from "./extensions/code-block";
import { Codemark } from "./extensions/code-mark"; import { Codemark } from "./extensions/code-mark";
import { DateTime, DateTimeOptions } from "./extensions/date-time"; import { DateTime, DateTimeOptions } from "./extensions/date-time";
@@ -76,7 +75,6 @@ import { useToolbarStore } from "./toolbar/stores/toolbar-store";
import { DownloadOptions } from "./utils/downloader"; import { DownloadOptions } from "./utils/downloader";
import { Heading } from "./extensions/heading"; import { Heading } from "./extensions/heading";
import Clipboard, { ClipboardOptions } from "./extensions/clipboard"; import Clipboard, { ClipboardOptions } from "./extensions/clipboard";
import { convertBrToParagraph } from "./utils/html";
import Blockquote from "./extensions/blockquote"; import Blockquote from "./extensions/blockquote";
declare global { declare global {
@@ -147,14 +145,10 @@ const useTiptap = (
() => ({ () => ({
enableCoreExtensions: false, enableCoreExtensions: false,
editorProps: { editorProps: {
...editorProps, ...editorProps
transformPastedHTML(html) {
return convertBrToParagraph(html).documentElement.outerHTML;
}
}, },
extensions: [ extensions: [
...CoreExtensions, ...CoreExtensions,
ClipboardTextSerializer,
NodeViewSelectionNotifier, NodeViewSelectionNotifier,
SearchReplace, SearchReplace,
TextStyle.extend({ TextStyle.extend({

View File

@@ -1,45 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`convert br tags to paragraphs (
<html><body>
<!--StartFragment-->A troll, they call me, but I have no wish<br>
to be associated with those dolls<br>
<br>
We lack religion, purpose, politics,<br>
and yet, we somehow manage to get by.<br>
</body>
</html>) 1`] = `"<p><!--StartFragment-->A troll, they call me, but I have no wish</p><p data-spacing=\\"single\\">to be associated with those dolls</p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\">We lack religion, purpose, politics,</p><p data-spacing=\\"single\\">and yet, we somehow manage to get by.</p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"> </p>"`;
exports[`convert br tags to paragraphs (<html><body>
<!--StartFragment--><p dir="auto">When I try to paste something (e.g. email content) to a note, the styling is kept, which is good, but the newlines are removed.<br>
Also when I share the selection to Notesnook via the share functionality from Android, I have the same issue.</p>
<hr>
<p dir="auto"><strong>Device information:</strong><br>
App version: 2.3.0<br>
Platform: android<br>
Model: OnePlus-CPH2409-31<br>
Pro: true<br>
Logged in: yes</p><!--EndFragment-->
</body>
</html>) 1`] = `"<!--StartFragment--><p dir=\\"auto\\">When I try to paste something (e.g. email content) to a note, the styling is kept, which is good, but the newlines are removed.</p><p data-spacing=\\"single\\"> Also when I share the selection to Notesnook via the share functionality from Android, I have the same issue.</p> <hr> <p dir=\\"auto\\"><strong>Device information:</strong></p><p data-spacing=\\"single\\"> App version: 2.3.0</p><p data-spacing=\\"single\\"> Platform: android</p><p data-spacing=\\"single\\"> Model: OnePlus-CPH2409-31</p><p data-spacing=\\"single\\"> Pro: true</p><p data-spacing=\\"single\\"> Logged in: yes</p><!--EndFragment-->"`;
exports[`convert br tags to paragraphs (<html><body>
<!--StartFragment--><span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">Why switch from Gmail?
Not sacrificing features for more privacy, prefer using one app, in many public groups and channels (Telegram)
LibreOffice Slow &amp; buggy
Switched to Brave for the better Android app, more private out of the box &amp; unsure if uBlock Origin closes gap</span><!--EndFragment-->
</body>
</html>) 1`] = `"<!--StartFragment--><span class=\\"css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0\\"><p>Why switch from Gmail? </p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"> Not sacrificing features for more privacy, prefer using one app, in many public groups and channels (Telegram)</p><p data-spacing=\\"single\\"> </p><p data-spacing=\\"single\\"> LibreOffice Slow &amp; buggy</p><p data-spacing=\\"single\\"> </p><p data-spacing=\\"single\\"> Switched to Brave for the better Android app, more private out of the box &amp; unsure if uBlock Origin closes gap</p></span><!--EndFragment-->"`;
exports[`convert br tags to paragraphs (<p><br/></p>) 1`] = `"<p><br></p>"`;
exports[`convert br tags to paragraphs (<p>line <em>1<br>line</em> 2</p>) 1`] = `"<p>line <em>1</em></p><p data-spacing=\\"single\\"><em>line</em> 2</p>"`;
exports[`convert br tags to paragraphs (<p>line <span><em>1<br data-some="hello">line</em></span> 2</p>) 1`] = `"<p>line <span><em>1</em></span></p><p data-spacing=\\"single\\"><span><em>line</em></span> 2</p>"`;
exports[`convert br tags to paragraphs (<p>line <span><em>1<br>line</em></span> 2</p>) 1`] = `"<p>line <span><em>1</em></span></p><p data-spacing=\\"single\\"><span><em>line</em></span> 2</p>"`;
exports[`convert br tags to paragraphs (<p>line 1<br>line 2</p>) 1`] = `"<p>line 1</p><p data-spacing=\\"single\\">line 2</p>"`;