Some pointers in creating a casing plugin

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 = state.tr;
    const selection = tr.selection;

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

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

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

        // grab the content
        const substringFrom = Math.max(0, selection.from - position - 1);
        const substringTo = Math.max(0, selection.to - 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) {
        dispatch(tr.scrollIntoView());
    }
}
2 Likes