Understanding node structure and positions

I’m running into trouble writing some fairly simple transaction code, that makes me think I haven’t properly groked the way positions and the node hierarchy work together.

Suppose I have a document with nested block nodes (e.g. list, list items, paragraphs) and I want to set some attribute on every node.

The following code gives me a “No node at given position” RangeError on some documents. I think I’m not correctly taking into account the start and end markers, but I can’t figure out the solution.

function addAttr(node, pos, tr) {
  if (node.isBlock) {
    tr.setNodeMarkup(pos, null, {...node.attrs, myAttr: "yup"})
    node.forEach((n, offset) => {
      addAttr(n, pos+offset, tr)
    })
  }
}

I kick it off with addAttr(doc, 0, state.tr)

Thanks for any advice!

The solution I’ve come to, though I’m not super confident about it, is

function addAttr(node, pos, tr) {
  if (node.isBlock) {
    tr.setNodeMarkup(pos-1, null, {...node.attrs, myAttr: "yup"}) // Note -1
    node.forEach((n, offset) => {
      addAttr(n, pos+offset+1, tr) // add 1 for the block node start
    })
  }
}

(I’ve also noticed there is node.descendants which would be better here, but it would still be useful to know if the logic is correct)

I would look at Fragment.nodesBetween, which is what Node.descendants calls.

Having done quite similar things, it goes something like

doc.nodesBetween(blockPos, blockPos + blockNode.nodeSize, (node, pos) => {
  if (node.isBlock) {
    tr.setNodeMarkup(pos, undefined, {...node.attrs, myAttr: "yup"}, node.marks)
  }
})

This will start iterating from the node at that position so you won’t have to set its attributes separately.

Your original code was indeed ignoring the 1 token for the opening node. However, your new code still looks like it will try to set an attribute on the top document node at -1, which won’t work. Also there seems to be remaining confusion about where to add the extra 1 position for the opening token—pos - 1 suggests you’re passing the position of the start of the node’s content to your recursive function, but then you add + 1 in the forEach, which only makes sense if pos is the node’s actual start position.

I think I’ve figured this out now. If you want to walk the node tree with a recursive function, passing a node and its start position on each call, then you naturally want to start the recursion with doc as the root node. But the doc node is unlike other nodes in that there is no opening node position, so you either need some conditional logic to only +1 when the node is not the document, or simply pass -1 as the initial value.

@marijn point taken about setting attributes on the document. That was only a problem with my simplified example code.