Odd copy/paste and node wrapping interaction

My oh-so-clever footnote implementation that was working so well in testing, has bitten me hard on the first day in production.

Where I got clever was allowing my footnote to have a content expression of ‘block+’ instead of inline. Then I edit that content in a separate prosemirror instance before saving it back into my footnote node. While this works well for the footnotes themselves when I paste in multiple paragraphs (or any complex html), my footnote node spec allows itself to become a wrapper for the pasted content. If I select the entire document and replace it with the pasted content it shows up in the top-level doc node as expected, but by default there’s an empty paragraph node and that’s where the wrapping behavior seems to kick in.

I can work around this by limiting the footnotes to inline content, but I wonder if this is a sign I should go back to my original implementation which had the footnote content inside it’s own doc as an attr on the footnote vs as a content prop. It makes the rendering a little more complex on our front ends, but still simple enough to groc what’s happening.

Alternatively I wonder is there a way to tell findWrapping that a footnote isn’t allowed to be auto-inferred?

Interestingly if I remove the content expression from my node entirely it no longer is used as a wrapper. Since I set its content entirely in a plugin that seems to work just fine since the view just ignores it. Is that a safe solution or am I playing with fire by going so far outside normal node behavior?

Could this be another instance of this issue? The HTML parser will close outer paragraphs when it finds a nested <p> tag, because that happens to be how it’s specified. You might be able to work around this by using another HTML tag for your paragraph nodes (and other block nodes that have the same HTML parser behavior).

As in if I pre-process the incoming paragraphs? My internal schema already has replaced p tags with custom paragraph tags in its paragraph node.

Here’s my paragraph node

// This node exists to support paragraphs containing
// footnotes which can contain paragraphs which confuses
// the heck out of the DOM parser when copy pasta-ing html
// in the editor. For discussion see:
// https://discuss.prosemirror.net/t/pasting-footnotes-with-content/2479
const paragraphNodeSpec = {
  content: 'inline*',
  group: 'block',
  parseDOM: [
    {tag: 'paragraph'},
    {tag: 'p'}
  ],
  toDOM() {
    return ['paragraph', 0];
  }
}

module.exports = paragraphNodeSpec

and my footnote

// This node depends on a custom node view
// the parseDOM rules here are only
// used for drag-n-drop and copy/paste
// toDOM exists purely for spec purposes
const {Fragment} = require('prosemirror-model')

const apmFootnoteNodeSpec = {
  type: 'apm_footnote',
  content: 'block+',
  group: 'inline',
  isolating: true,
  defining: true,
  inline: true,
  atom: true,
  attrs: {
    number: { default: null }
  },
  parseDOM: [{
    getAttrs(dom) {
      return { number: parseInt(dom.dataset.number) }
    },
    getContent(dom, schema) {
      return Fragment.fromJSON(schema, JSON.parse(dom.dataset.footnote_content))
    },
    tag: "footnote"
  }],
  toDOM: (node) => {
    return [
      "footnote",
      {
        contentEditable: false,
        "data-number": node.attrs.number,
        "data-footnote_content": JSON.stringify(node.content.toJSON())
      },
      ["footnotehead", `Footnote ${node.attrs.number}`],
    ]
  }
}

module.exports = apmFootnoteNodeSpec

Interestingly when I paste from a simple html doc like:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title></title>
</head>
<body>
<p>graph one</p>
<br>
<p>graph two</p>
<br>
<p>graph three</p>
<br>
<ul>
<li>one
<li>two
<li>three
</ul>
</body>
</html>

I get the following from transformPastedHTML

<p style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: &quot;Avenir Next&quot;, Helvetica, Arial, sans-serif; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(254, 254, 254); text-decoration: none;">graph one</p><br style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: &quot;Avenir Next&quot;, Helvetica, Arial, sans-serif; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(254, 254, 254); text-decoration: none;"><p style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: &quot;Avenir Next&quot;, Helvetica, Arial, sans-serif; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(254, 254, 254); text-decoration: none;">graph two</p><br style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: &quot;Avenir Next&quot;, Helvetica, Arial, sans-serif; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(254, 254, 254); text-decoration: none;"><p style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: &quot;Avenir Next&quot;, Helvetica, Arial, sans-serif; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(254, 254, 254); text-decoration: none;">graph three</p><br style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: &quot;Avenir Next&quot;, Helvetica, Arial, sans-serif; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(254, 254, 254); text-decoration: none;"><ul style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: &quot;Avenir Next&quot;, Helvetica, Arial, sans-serif; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(254, 254, 254); text-decoration: none;"><li>one</li><li>two</li><li>three</li></ul>

If I switch the <p> tags to <paragraph> and past I get

3 paragraphs and one footnote (wrapped over the unordered list)

My suspicion is still that I either need to stop allowing block content inside an inline node entirely or, if it doesn’t seem too reckless, to stop declaring that my inline node has block content since I’m not relying on the view to process it.

Heh, removing the content expression makes copy-pasting of footnotes go sideways (which makes sense). I’ve reverted my implementation to using an attr with a doc node inside it and that works great. The one downside is I can’t have footnotes inside of footnotes, but that was always going to be dicey at best.