Patterns for rendering subsets of a document

Lately, I’ve been interested in exploring techniques for rendering a partial ProseMirror doc. My theory is that a good API around this could open up functionality around:

  • Node-level subsets: “focusing in” on a part of the tree, common in outliners, a one-chapter-at-a-time book editor, presentation slides
  • Slice or fragment-level ranges: useful for diff-style views, document search with context, visual clipboards, print previews
  • Derivative documents: where the parent document has been transformed into a new shape, like a table of contents, a document navigator, aggregated text with comments; maybe even the basis of pagination (this is view-model territory)

What each of these scenarios has in common: we have one or more EditorView instances that need to intercept transactions and map their steps to be applied against a source document. The original document is updated, and then this state is carved up or transformed and passed as a prop to the sub-views. The value in this is that a single document can be persisted and set up for collaboration, rather than a bunch of individual independent pieces of state stitched together out-of-band.

This is all very abstract, so it’s probably best to anchor this conversation with an example. Here is a “book editor”, where a single chapter can be focused at a time, and a table of contents editor selects the chapter and keeps their titles in sync:

This largely works, but I’ve run into some rough edges I’d like to sand down. I’ve tried to keep track of these as github issues so far:

  • Schema coupling: steps can only be applied across views if the NodeType instances are the same object. In practice this means every view’s structural types have to be defined in a single schema, even when they’re purely a view concern (like the toc_doc top node).
  • Node type mismatch between source and view: the table of contents reuses heading nodes so steps transfer, but they render as <h1> in both views. Semantically they should be list items.
  • Selection doesn’t survive document rebuilds: Selection is bound to a specific document instance in ResolvedPos, so scoped views can’t carry a selection across state rebuilds. Both plugins work around this by stashing raw position integers before dispatching and recreating the selection afterward.
  • Step remapping abstraction: every scoped view has a dispatchTransaction that loops over steps, offsets them, and dispatches on the outer view. The logic is nearly identical between both plugins. If some of this were extracted into a library, what would it’s API look like?

I am definitely not asking for anyone to solve these issues, but if you also find yourself in this design space, I’d love to hear your thoughts (and if you’ve attempted anything similar). I haven’t seen a whole lot of other chatter around this problem in general (this and that come close), but I’m sure the design of all of this could improve by knowing about more use cases.