How to make changes that temporarily violate the schema?

Hi,

I am trying to create a command that turns the current paragraph into a heading and wraps it into a section. I would like to specify in the schema that a section starts with a heading followed by blocks.

So I have a heading NodeType with content 'inline*' that belongs to the group 'heading', and a section NodeType with content 'heading block*' that belongs to group 'block’.

To turn a paragraph into a section heading, I need to also wrap it into a section. Unfortunately tr.setNodeMarkup(pos, HeadingType).wrap(paragraph, {type: SectionType}) does not work, because turning the paragraph into a heading before it is wrapped in a section violates the schema. As does the reverse (wrapping the paragraph in a section and then turning it into a heading).

If I change the group of heading to be 'block' and the content of Section to block*, then it works, but I loose the benefit of the schema enforcing a heading at the beginning of the section.

So my question boils down to: how to make changes that temporary violate the schema? Do I need to create a special step to do this?

Thanks in advance for your help.

const sectionNode = schema.nodes[sectionNodeTypeName].create({}, [
   schema.nodes[headingNodeType].createAndFill(),
   paragraphNode
])
tr.replaceWith(pos, pos + tr.doc.nodeAt(pos).nodeSize), sectionNode)

Hope this helps.

Thanks! sounds good, I’ll try it and report.

I was hung up on turning the paragraph into a heading and didn’t think of that approach.

That is not really something the library supports, and the approach provided above is likely to cause problems if you do anything with the invalid document (and will possibly break undo or collab even if immediately followed by a step that moves the document to a valid state again).

The intended way to make this kind of change would be to create a ReplaceAroundStep that combines the wrapping and the adding of the header in a single step. Building such a step is somewhat finicky, unfortunately.

Mmmh, my editor is collaborative so I need to be careful with that.

However the solution proposed by @shinTSY does not put the document in an invalid state, does it?

Ah, no, it seems it just copies a part of the document into the new node. But that’ll cause other collab issues, because it doesn’t know that the content of paragraphNode was preserved, so any concurrent changes to that will be overwritten by such a step.

I see.

So would it work to do it like this:

  • First step: use a ReplaceAroundStep to wrap the content of the first paragraph node into a header node and a section node. Using HTML-like notation, it would turn
<p>section title</p> ... text ...

into

<s><h>section title</h></s> ... text ...
  • Second step: use another ReplaceAroundStep to extend the scope of the section so it includes the following paragraphs. In other words
</s> ... text ...

becomes

... text ... </s>

so that we end up with

<s><h>section title</h> ... text ... </s>

I am not sure the latter is possible though?

It sounds like the first replace-around step could also immediately put the section ending token in the right place, avoiding the need for the second one.

I don’t see how to do that without copying text because I need the end of the heading after the title, and the end of the section after the text. So to me it looks like I need a ReplaceAround with 2 gaps.

Or are you suggesting that I modify the slice before calling Replace Around?

Oh, I see, you’re changing the type of an existing block to heading, not inserting an empty heading. Yeah, then you are right.

It may be a more productive direction to relax your schema constraints, also for ease of editing (so users don’t have to understand a set of complicated transformation commands to create the documents they are interested in), and treat the desired structure more like a lint than a hard constraint.

Yes, as Margin said, you need to use ReplaceAroundStep if you want to avoid impact on collaborators. I don’t know if you have used ReplaceAroundStep before and I maybe misunderstood your intention, so i didn’t mention this step.

Thank you both. I have relaxed the schema so that the heading is optional. This way, I can wrap the content of the section into a Section node first and then turn the first paragraph into a Heading.

I am struggling with another issue though: the image on the left shows a text with 3 nested sections (the bars on the left show the scope). When the user turns the line “Seven” into a heading at level 1, she gets the picture on the right: a new section at level 1 has been created, until the end of the document. I do that by using a split of depth 3 (and then wrapping lines Seven to Ten in a section and turning line Seven into a heading).

Now I want the user to be able to do the reverse, i.e. to turn the line Seven on the right into plain text and get back to the image on the left. I don’t know how to do this efficiently: if I do a joinBackward, I get the picture below on the left, where the lines Seven to Ten are nested in the level-1 section, not the level-3 one. And if I do another 2 joinBackward, only the line Seven gets within the level-3 section (each line is a “block” in this particular editor), as shown below on the right. I could keep calling joinBackward to “pull” the lines Eight to Ten into the level-3 section, but that seems inefficient at best…

Section2

Do you think I can use replaceAround to do this? Here’s the sketch I have in mind:

... text1 </s3></s2></s1><s1> text2 </s1>
         [                   ]     [     ]
       from              gapFrom  gapTo To
slice:
         []insert[</s3></s2></s1>]
gives
... text1 text2 </s3></s2></s1>

Thanks for your help and patience :slight_smile:

What’s your input(DOM structure) and the output you expected(DOM structure)?

In my opinion, ReplaceAroundStep just care about these and the range you want to preserve.