Getting a hold of the `document` used by ProseMirror from `toDOM()`

  1. Getting a hold of DOM serializer’s document in toDOM() seems to involve:

    • serializeNodeInner() (pass document to node spec’s toDOM()),
    • serializeNode() (pass document to serializeMark()),
    • serializeMark() (pass document to mark spec’s toDOM()), and
    • updating node/mark spec’s toDOM() TypeScript signature (backwards compatible).

    For example, in case of serializeNodeInner(), just this call (lines split for legibility) would change from

    renderSpec(
      doc(options),
      this.nodes[node.type.name](node),
      null,
      node.attrs)
    

    to

    const dom = doc(options)
    renderSpec(
      dom,
      this.nodes[node.type.name](node, dom),
      null,
      node.attrs)
    
  2. Regarding what document entails, for ProseMirror’s purposes (meaning both what callers must pass to serializeNode() and what it’d pass to spec’s toDOM() per (1) above) it seems to be a tiny subset of Document with only the following:

    • .createTextNode() (called here)
    • .createDocumentFragment() (called here)
    • .createElement[NS]() (called here, and presumably in a toDOM() that wishes to return a DOM node)

    Concerning the HTMLElement or DocumentFragment returned from the above functions, for PM’s purposes it seems they only must implement:

    • .nodeType property, only applicable to HTMLElement
    • .setAttribute[NS]() (called here), only applicable to HTMLElement
    • .appendChild() (called here and here)

    This is based on prosemirror-model’s to_dom.ts, let me know if there’re other modules of concern.

    Narrowing down the type can be done easily with TypeScript’s built-in Pick type:

    type BareDocument = Pick<Document,
      | "createTextNode"
      | "createDocumentFragment"
      | ...>;
    

Notes;

  • The above changes are backward compatible and don’t affect existing users.

  • The changes are independent of each other, though I would say it’d be better to narrow down Document first, if it’s to be added to toDOM()’s signature.

  • In fact, instead of giving toDOM() access to document, even a narrowed-down version, it could be worth passing just an element constructor function:

    toDOM(
      node: Node,
      createDOMElement: BareDocument["createElement"],
    ): ...