Automatic joining of marks with attributes


I’ve been heavily using marks with attributes and found the automatic joining does not always produce what you might expect. Specifically, I’ve found an edge-case where a simple replace will set all marks’ attributes inside a paragraph to their default values.

It’s pretty weird. Here’s a reproduction React App

  1. Click ‘Restore example’
  2. Select ‘po’
  3. Press ‘m’
  4. All marks should lose their background

The doc in question:

Also when you delete regular text next to a mark and then type text again, it will not receive marks from the left side. Moving the cursor next to the mark and then typing, will on the other hand receive the marks. Not a dramatic effect but thought it’s interesting.

I recall reading somewhere that marks should only be joined if all their attributes equal each other? Was it written in the docs? And it did do deep equality checks for objects as well?

Without seeing what code is being run, there’s not much I can say about this.

Marks are considered equal if their type and attributes are equal. Attribute content is compared with a simple deep-equal function.

I tried to find what function handles the default keypresses and creates the ReplaceStep, can you point me in the right direction so I can write it down manually? Removing exampleSetup doesn’t change the behaviour at least. I suppose I could also add prosemirror-view or state as a submodule and add a debugger statement somewhere to be able to intercept the transaction as it’s being created.

What happens on normal text input is that the browser handles it natively, and then the code in prosemirror-view/src/domchange.js diffs the new DOM with the current document and synthesizes a transaction from it, probably going through replaceSelection.

Okay I updated the example to include console.debug statements to show the created MutationRecords. It’s a bit difficult to understand what causes this misbehaviour to occur but it seems the findDiff receives rather large fragments to diff. Which then joins the adjacent marks of same type without taking into account their different attributes.

What specifically do you want to see to verify this problem? I’m not really familiar with how these MutationRecords are handled but from the looks of it, the from & to of the change is way too large for the change that occurred.

A small stand-alone script that I can run on my side to debug this. Debugging opaque systems that might be doing who-knows-what is too time-consuming.

Hey @marijn I finally came around making a simpler reproduction. I might have encountered a similar issue in different context that may or may not be related. EDIT: Oh yeah you asked for a stand-alone script. Well I can provide this as well if needed.

Your parseDOM on the colored italic mark doesn’t parse the color attribute at all, which probably explains why it is being dropped here.

Aaa… I see. But I’m a bit thrown off how parseDOM is triggered all of the sudden. I mean shouldn’t the EditorState be the source of the truth? So even though I’m not persisting the attributes to the DOM it shouldn’t matter as they are still kept in the state?

Chrome appears to be doing some superfluous DOM insertions and removals on such a change. The issue doesn’t occur in Firefox. When the DOM is messed with, ProseMirror will re-parse it to figure out what the updated document looks like.

Well that sucks. But are you sure about it being a Chrome issue? I’m able to reproduce it in every browser I tried, including Firefox (macOS and Windows) and Safari.

Not sure about your app, but in the dino example I don’t see this in Firefox.

Oh yeah, now I see what you mean. Depending on have you selected/focused on the editor and what character you press (sometimes ‘m’ works, sometimes say ‘f’) it sometimes keeps the attributes, sometimes not in Firefox. But it still does fail similar to Chrome and Safari.

What a nightmare. But is there any way of detecting when these things happen?

I reduced the example into single html file

Why is it a problem to define a working DOM deserialization for your mark, though?

It’s a relatively large object of data that’s not needed in DOM but sure, I suppose I should do that. It’s just this is quite unexpected behavior. Should I also add it to nodes as well? If attributes can disappear unless you explicitly persist them to DOM that should read in big letters somewhere.