Replace Image With Text On Text Copy

I am trying to write an inline NodeSpec to replace emoji unicode characters with inline images. It works for pasting emoji characters, since we can use the transformPasted prop to detect and then insert emoji nodes.

Is there a way to write the opposite equivalent, like a transformCopied, that could be used to transform an emoji node back into a textual representation?

(I was partially motivated by seeing github use inline images for emojis on its blog. If you scroll to the post script and copy the last paragraph which has an image representation :sparkles:, it’s alt is copied if you paste it into an editor or url bar.)

Edit: It also works here on discuss if you copy and paste the above sparkles emoji. I initially thought it may be a default browser behavior of copying the image alt, but it doesn’t work with prosemirror in contenteditable true or false when I have an inline emoji image with an alt specified.

Have you tried clipboardTextSerializer?

I did look at that method, but because its function scheme is fn(Slice) → String, I assumed it would override the text serializing for all nodes instead of just the one for emojis.

In contrast with transformPasted: fn(Slice) → Slice, each node could have its own transformPasted prop defined with a separate plugin so

😭 http://localhost:8080/

could be parsed properly with emojify(linkify(Slice)) as an example.

Yes, it does—there currently isn’t a way to customize text serialization per-node.

Would that be something prosemirror is willing to support? At a glance at prosemirror-view/clipboard.js, it would be similar to transformPaste with the opposite ordering (?):

  view.someProp("transformCopied", f => { slice = f(slice) })
  let text = view.someProp("clipboardTextSerializer", f => f(slice)) ||
      slice.content.textBetween(0, slice.content.size, "\n\n")
  return text

If not, that would also be fine since it would be easy (?) for me to shim this in a library that builds on top / outside of prosemirror: instead of transformCopied being part of separate plugins, each node or mark could define its own method which is then collected into a singular transformCopied plugin (sort of like inputrules or keys).

I think I’d prefer to keep this external to the library for the time being (though I might reconsider if this specific feature comes up again at some point).

1 Like

Hi there, I implemented my own textBetween method and provide it as a clipboardTextSerialize myself: It checks the presence of a value attribute when a node is a leaf node and use this value as a text.

import {
  Node as ProseMirrorNode,
  DOMOutputSpec,
  NodeSpec,
  DOMSerializer,
  Slice,
  Fragment,
} from 'prosemirror-model';

  function customTextBetween(
    fragment: Fragment,
    from: number,
    to: number,
    blockSeparator: string,
    leafText?: string
  ) {
    let text = '',
      separated = true;
    fragment.nodesBetween(
      from,
      to,
      (node, pos) => {
        if (node.isText) {
          text += node.text?.slice(Math.max(from, pos) - pos, to - pos);
          separated = !blockSeparator;
        } else if (node.isLeaf && node.attrs.value != undefined) {
          console.log('LEAF NODE WITH VALUE:', node);
          text += ' '+node.attrs.value+' ';
          separated = !blockSeparator;
        } else if (node.isLeaf && leafText) {
          console.log('ORIGINAL LEAF NODE BEHAVIOUR:', node);
          text += leafText;
          separated = !blockSeparator;
        } else if (!separated && node.isBlock) {
          text += blockSeparator;
          separated = true;
        }
      },
      0
    );
    return text;
  }

...
// in the Editor props:

          clipboardTextSerializer: (slice: Slice) => {
            return customTextBetween(
              slice.content,
              0,
              slice.content.size,
              '\n\n',
              '<classic leaf replacement text>'
            );
          },

Hope it helps someone! :wink:

2 Likes