Some pointers in creating a casing plugin

A little lost in ProseMirror at the moment and trying to understand the correct way to replace the text of the current selection. I want to be able to toggle the case between lower/upper case.

state.selection.content(); will return slice of the relevant nodes under the selection, and the content that is selected, but does not return the range within each node that would need to replaced.

I assume I would need to create a new text node to replace the range within each selected node something like:

const updatedText = node.textContent.toUpperCase();
const textNode = state.schema.text(updatedText);
transaction = transaction.replaceWith(startPos, startPos + node.nodeSize, textNode);

How do I get the range to replace within each node?

Unfortunately, I cannot find a suitable example or existing plugin to review. Very good chance I am approaching this incorrectly too!

I think calling nodesBetween on the selected range and replacing each text node you find with an uppercased variant (keeping marks per node, so something like schema.text(textNode.text.toUpperCase(), textNode.marks)) would be the most straightforward way to do this.

Possibly better ways to do this, but sharing the following for future users:

const execute = (casing, state, dispatch) => {
    // grab the current transaction and selection
    let tr =;
    const selection = tr.selection;

    // check we will actually need a to dispatch transaction
    let shouldUpdate = false;

    state.doc.nodesBetween(selection.from,, (node, position) => {
        // we only processing text, must be a selection
        if (!node.isTextblock || selection.from === return;

        // calculate the section to replace
        const startPosition = Math.max(position + 1, selection.from);
        const endPosition = Math.min(position + node.nodeSize,;

        // grab the content
        const substringFrom = Math.max(0, selection.from - position - 1);
        const substringTo = Math.max(0, - position - 1);
        const updatedText = node.textContent.substring(substringFrom, substringTo);

        // set the casing
        const textNode = (casing === 'uppercase')
            ? state.schema.text(updatedText.toUpperCase(), node.marks)
            : state.schema.text(updatedText.toLocaleLowerCase(), node.marks);

        // replace
        tr = tr.replaceWith(startPosition, endPosition, textNode);
        shouldUpdate = true;

    if (dispatch && shouldUpdate) {