Describe the bug
When attempting to add a pasteRule for a Node, it does not work as the pasteRule helper function have a call to a Mark node (note how we call addToSet on the created node, which may not be a mark but a node):
nodes.push(child.cut(start, end).mark(type.create(attrs).addToSet(child.marks)));
Steps to Reproduce / Codesandbox Example
pasteRules({type}) {
return [pasteRule(/SOME_YOUTUBE_REGEX/g, type, (url) => ({src: url}))]
}
https://www.youtube.com/watch?v=H08tGjXNHO4Uncaught TypeError: type.create(...).addToSet is not a functionExpected behavior
a new MediaEmbed node should be created with the given attrs
@philippkuehn I've created a simple helper function to solve that:
export function nodePasteRule(regexp, type, getAttrs) {
const handler = fragment => {
const nodes = [];
fragment.forEach(child => {
if (child.isText) {
const {text} = child;
let pos = 0;
let match;
// eslint-disable-next-line
while ((match = regexp.exec(text)) !== null) {
if (match[0]) {
const start = match.index;
const end = start + match[0].length;
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
// adding text before markdown to nodes
if (start > 0) {
nodes.push(child.cut(pos, start));
}
// create the node
nodes.push(type.create(attrs));
pos = end;
}
}
// adding rest of text to nodes
if (pos < text.length) {
nodes.push(child.cut(pos));
}
} else {
nodes.push(child.copy(handler(child.content)));
}
});
return Fragment.fromArray(nodes);
};
return new Plugin({
props: {
transformPasted: slice => new Slice(handler(slice.content), slice.openStart, slice.openEnd),
},
});
}
Basically it is a very similar function as markPasteRule but it just creates the node with the given attribute instead of trying to add a mark ~and does not contain a while loop. Even in the markPasteRule I'm not sure why there is a while loop when match is only every assigned once?~ It seems like the while loop is for the regexp.exec call due to it attempting to match from the lastIndex: https://stackoverflow.com/questions/1520800/why-does-a-regexp-with-global-flag-give-wrong-results
I guess that if you want you could extract the common logic between those 2 methods and just pass a handler to create the node/mark.
What do you think?
@kfirba Nice shot!
Is it possible to show some examples about nodePasteRule ?
@Alecyrus The same as the regular markPasteRule:
pasteRules({type}) {
return [
nodePasteRule(
SOME_REGEX_TO_MATCH_THE_NODE,
type,
match => {
// return some attrs, if any.
},
),
];
}
@kfirba Hi dude, I need some help.
I want to support pasting Heading node by matching string like # asdasda \n## asdasd. and some errors occur. If I use the following code, there will be endless output in console.
while ((match = regexp.exec(text)) !== null) {
if (match[0]) {
const start = match.index;
const end = start + match[0].length;
// match[1].length is for parsing the level of Heading node
const attrs = getAttrs instanceof Function ? getAttrs(match, match[1].length) : getAttrs;
// adding text before markdown to nodes
if (start > 0) {
nodes.push(child.cut(pos, start));
}
// create the node
nodes.push(type.create(attrs));
pos = end;
}
}
If I change while to if, it seems to work, but the nodes inserted into document are not as i expect. the nodes(Fragment) returned by handler are wrapped by a Paragraph Node.
//... Heading.js
pasteRules({ type }) {
return [
nodePasteRule(new RegExp(`^(#{1,3})(.*)`), type, (match, level) => ({
level,
}))]
}
//... nodePasteRule.js
import { Plugin } from 'prosemirror-state'
import { Slice, Fragment } from 'prosemirror-model'
export function nodePasteRule(regexp, type, getAttrs) {
const handler = fragment => {
const nodes = [];
fragment.forEach(child => {
if (child.isText) {
const { text } = child;
let pos = 0;
let match;
console.log("text", text)
console.log("regexp", regexp)
console.log("res", regexp.exec(text))
// eslint-disable-next-line
while ((match = regexp.exec(text)) !== null) {
console.log(match[0])
if (match[0]) {
const start = match.index;
const end = start + match[0].length;
const attrs = getAttrs instanceof Function ? getAttrs(match[0], match[1].length) : getAttrs;
// adding text before markdown to nodes
if (start > 0) {
nodes.push(child.cut(pos, start));
}
console.log(type)
// create the node
nodes.push(type.create(attrs));
pos = end;
}
}
// adding rest of text to nodes
if (pos < text.length) {
nodes.push(child.cut(pos));
}
} else {
nodes.push(child.copy(handler(child.content)));
}
});
console.log(nodes)
return Fragment.fromArray(nodes);
};
return new Plugin({
props: {
transformPasted: slice => new Slice(handler(slice.content), slice.openStart, slice.openEnd),
},
});
}
@Alecyrus I think you get an endless loop as you didn't provide the g flag for your regex. Try replacing your regex to this:
new RegExp(`^(#{1,3})(.*)`, 'g')
Can you show some input => output example?
Also, if you could provide a reproduction it would be best.
@kfirba The first problem is solved, but the second problem is .... uh.. I don't know how to describe. If possible, could you please show me how to implement the following feature:
content:
# Heading 1
## Heading 2
Then pasting these text in our own editor, which could render two nodes (Heading Node with level 1 and 2):
# Heading1
Heading2
I notice the following code
// adding text before markdown to nodes
if (start > 0) {
nodes.push(child.cut(pos, start));
}
In my case, the output is:

The value of start is 0. And this rule seems not handle something line-wrapped markdown or tight lists.
@Alecyrus Hey.
I tried solving this but I just don't know how to create a node and append a text node to it. I got close, but could not find how to glue the last part.
The code I used: (I've slightly modified the regex and extracted the correct heading level)
nodePasteRule(
/^#{1,3} ?(.*)/g,
type,
match => {
let level = 0;
for (let i = 0; i < match[0].length; i++) {
if (match[0][i] !== '#') break;
level++;
}
return {level}
},
)
As for the nodePasteRule code, I've modified it a bit to get close. I create the node type (type.create(attrs)) and then create a text node containing the text:
// ...
const node = type.create(attrs);
const textNode = child.cut(start, end);
// How do I set the `textNode` as the `node` content? No idea :/
// ...
I just don't know how to glue the last piece of the puzzle. If you know how to do it it would be great if you share it with me :)
Maybe you can open a thread in ProseMirror forums? The maintainer is very quick to answer!
There is already a thread about supporting pasting markdown text, Support pasting markdown.
And I have another idea about this feature. You know, when you select heading texts ( \
So I am wondering... how about transforming text to special text with styles, not just pasting plain text. And the prosemirror can handle these styled text. The work we need to do is implementing the transformation algorithm.
I don't know whether it could do work, but I will give a try later.
@Alecyrus It seems a bit like overkill, in my opinion. There HAS to be a way to create a node with a text node in it. Had it been a transaction you could run insertText on the transaction. I wonder how it can be achieved during paste?
@Alecyrus Got it working?
I am a little busy these days, but I will figure it out.
@kfirba It do work. clipboardTextParser is a good way to implement the feature we need.
Check this thread, I am sure that you will find a solution when you open that thread.
...
clipboardTextParser: (str, $context) => {
...
// image Markdown
var _imageMarkdown = template('<img alt="<%= alt %>" src="<%= src %>" title="<%= title %>"></img>');
dom.innerHTML = _imageMarkdown({
alt: '',
src: 'http://img-operation.csdnimg.cn/csdn/silkroad/img/1589004076759.jpg',
title: 'Lorem'
});
var parser = DOMParser.fromSchema(this.editor.view.state.schema);
return parser.parseSlice(dom, { preserveWhitespace: true, context: $context });
},
...
Facing the same issue currently, did you ever got it to work @kfirba ?
Most helpful comment
@philippkuehn I've created a simple helper function to solve that:
Basically it is a very similar function as
markPasteRulebut it just creates the node with the given attribute instead of trying to add a mark ~and does not contain awhileloop. Even in themarkPasteRuleI'm not sure why there is awhileloop whenmatchis only every assigned once?~ It seems like thewhileloop is for theregexp.execcall due to it attempting to match from thelastIndex: https://stackoverflow.com/questions/1520800/why-does-a-regexp-with-global-flag-give-wrong-resultsI guess that if you want you could extract the common logic between those 2 methods and just pass a handler to create the node/mark.
What do you think?