Guide to the Document Data Structure

ProseMirror defines a set of data structures (in the prosemirror-model module) to represent documents. This guide explains those data structures.

Structure

A ProseMirror document is tree-shaped, much like the browser DOM. Documents are made up of nodes, each of which contains a fragment containing zero or more child nodes.

Each node is context-free, and does not know anything about its parent nodes. In fact, a node may be shared between multiple documents or even appear multiple times in a single document.

An important difference between the DOM and ProseMirror's data model is that inline content is flat in a ProseMirror document. The tree shape is not used to represent things like strong or emphasized text. Instead, text (and other inline content) can have marks associated with it to indicate such styling. In HTML, you'd have this:

<p>This is <strong>strong text with <em>emphasis</em></strong></p>

Conceptually, the structure of the corresponding ProseMirror paragraph looks like this:

<p>
  <span>This is </span>
  <span marks="strong">strong text with </span>
  <span marks="strong em">emphasis</span>
</p>

It is flat. This more closely matches the way we tend to think about and work with such text. It allows us to represent positions in a paragraph using a character offset rather than a path in a tree, and makes it easier to perform operations like splitting or changing the style of the content without performing awkward tree manipulation.

A full document is just a node. The document content is represented as the top-level node's child nodes. Often, it'll contain a series of block nodes, some of which may be textblocks that contain inline content. But the top-level node may also be a textblock itself, so that the document contains only inline content.

What kind of node is allowed where is determined by the document's schema.

Nodes

Each node has a type. This is an object containing information about the type of node that this is, including its name, various flags that indicate its role (isBlock, isTextblock, isInline, isText).

Each type may have attributes associated with it, which are values stored in every node of that type that provide more information about the node. For example, an image node might store its image URL in an attribute named src.

In addition, nodes come with an array of marks, which can add information like emphasis or being a link.

All nodes have a content property containing a fragment object, a collection of child nodes. For nodes that don't allow content, it will simply be empty.

Type, attributes, marks, and content are the data that make up a normal node obect. Text nodes also have a text property containing their text value.

Traversing

There are various ways in which you can run over the content of a node. For single-level traversal, the easiest way is to use the forEach method, which, much like Array.forEach, will call your function for each child node.

For iteration that isn't simply start-to-end, you can use indexing. The child method gives you the child at a given offset, and the childCount property tells you how many a node has.

To run over all nodes between a pair of positions, the nodesBetween method can be convenient. It will call a function for each descendant node in the given range.

Indexing

Positions in a document are represented as integers, indicating the amount of “tokens” that come before the given position. These tokens don't actually exist as objects, they are just a counting convention.

Interpreting such position involves quite a lot of counting. You can call Node.resolve to get a more descriptive data structure for a position. This data structure will tell you what the parent node of the position it, what its offset into that parent is, what ancestors the parent has, and a few other things.

Take good care to distinguish between child indices (as per childCount), document-wide positions, and node-local offsets (sometimes used in recursive functions to represent a position into the node that's currently being handled).

Changing

ProseMirror represents its documents as persistent data structures. That means, you should never mutate them. If you have a handle to a document (or node, or fragment) that object will stay the same.

This has a bunch of advantages. It makes it impossible to have an editor in an invalid intermediate state, since a new document can be swapped in instantaneously. It also makes it easier to reason about documents in a mathematical-like way, which is really hard if your values keep changing underneath you. For example, it allows ProseMirror to run a very efficient DOM update algorithm by comparing the last document it drew to the screen to the current document.

To create an updated version of a whole document, you'll usually want to use Node.replace, which replaces a given range of the document with a “slice” of new content.

To update a node shallowly, you can use its copy method, which creates a similar node with new content. Fragments also have various updating methods, such as replaceChild or append.

Or, if you need to leave a record of your changes (which is necessary when the document is in an editor), you'll go through the transform interface instead, which provides convenience methods for common types of transformations, and will immediately create a new document for every step.

So this is going to break things:

doc.nodeAt(pos).text += "!" // BAD!

Whereas this works:

new Transform(doc).insertText(pos, "!")