Design for constrained node content

I’m landing this on the master branch now. Here’s what you need to know to port your code:

Schema definition is done differently now. The SchemaSpec class is gone, and you now pass a plain object to the Schema constructor, something like this:

const mySchema = new Schema({
  nodes: { // Node types in the schema
    doc: {type: Doc, content: "block+"},
    paragraph: {type: Paragraph, content: "inline[_]*"},
    text: {type: Text},
    /* ... and so on */
  },
  groups: { // Groups referred to in content expressions
    block: ["paragraph", /* ... */],
    inline: ["text", /* ... */]
  },
  marks: {
     em: EmMark
     /* ... */
   }
})

(Content expressions are now roughly documented in a new guide.)

A bunch of things that were previously properties on node type classes are now expressed in these expressions in the schema definition. Nodes no longer have kind, contains, canBeEmpty, and containsMarks properties. The various canContain... predicates are replaced by a few finer-grained methods on nodes: canReplace, canAppend, and a contentMatchAt method for a lower-level interface for reasoning about its content. (But you’ll probably only need those when writing generic commands.)

(Moving some things off node type classes and into the schema definition is the first step in an effort to make those less magical. There’ll be more changes like that after 0.7.0.)

As a general rule, you now have to be more careful when modifying the document, since the more powerful constraints are also easier to violate. Whereas splitting nodes, for example, was almost always possible in the old model, that is no longer the case, and a new predicate canSplit was introduced in the transform package, allowing you to check in advance whether a split is safe.

Replace transformations have become more clever (this was the biggest challenge in implementing all this), and will preserve content constraints when necessary by inserting extra nodes on the edges of the replaced content. You should be able to use them without worrying too much about their inner working, and just rely on the fact that they’ll give you a valid document with the given content replacing the old range. (Of course, when you give them content that trivially fits, no magic will happen, and the content will be inserted exactly as expected.)

Another change that landed is that steps are represented differently. Again, you probably only need this for specialized code. Instead of all steps having the same fields, they are now classes in control of their own serialization, deserialization, and mapping. The amount of different step types has been reduced to 4, and changes that were previously expressed as an awkward series of split, join, and ancestor steps can now be done in a single ReplaceWrapStep, which replaces pieces of the document on both sides of a piece of content, allowing ‘motion’ of content between adjacent nodes, and wrapping/unwrapping of content, in a single step. This is important because the previous approach of using intermediate steps was likely to temporarily violate content constraints, making the steps impossible even though the end result was valid.

Let me know how this works for you. I plan to give you a few days to spot the most horrible problems, after which I’ll release 0.7.0.

2 Likes