Tiptap: PasteRule does not work for nodes

Created on 5 May 2020  ·  17Comments  ·  Source: ueberdosis/tiptap

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

  1. Add a paste rule to any node such as a media embed
pasteRules({type}) {
  return [pasteRule(/SOME_YOUTUBE_REGEX/g, type, (url) => ({src: url}))]
}
  1. try to paste a heading: https://www.youtube.com/watch?v=H08tGjXNHO4
  2. See error in console: Uncaught TypeError: type.create(...).addToSet is not a function

Expected behavior
a new MediaEmbed node should be created with the given attrs

feature request

Most helpful comment

@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?

All 17 comments

@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:

Copy from other editor

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:
截屏2020-05-06 10 02 44

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 ?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ageeye-cn picture ageeye-cn  ·  3Comments

winterdedavid picture winterdedavid  ·  3Comments

pk-pressf1 picture pk-pressf1  ·  3Comments

dolbex picture dolbex  ·  3Comments

connecteev picture connecteev  ·  3Comments