Lift the part of a node

Hi everyone! I have a schema

        doc: { content: 'block+' },
        paragraph: {
            content: 'inline*',
            group: 'block',
            parseDOM: [{ tag: 'p' }],
            toDOM() {
                return ['p', 0];
            },
        },
        blockquote: {
            content: 'paragraph+',
            group: 'block',
            defining: true,
            parseDOM: [{ tag: 'blockquote' }],
            toDOM() {
                return blockquoteDOM;
            },
        } as NodeSpec,
        code_block: {
            content: 'text*',
            marks: '',
            group: 'block',
            code: true,
            defining: true,
            parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
            toDOM() {
                return preDOM;
            },
        } as NodeSpec,
        text: { group: 'inline' },
    },

I have to implement blockquote and code_block formats. In a basic prosemirror example, if we select the part of any paragraph and click on code_block or blockquote, then the whole block is selected. It happens because under the hood, this example uses wrapIn command inside which we can see that code

let {$from, $to} = state.selection
let range = $from.blockRange($to) - range here is blocks around selection

In my case I need a bit different behaviour. I have to wrap in a block format the exact selection.

<p>some text</p> // select "text" and apply blockquote format
->
<p>some</p><blockquote><p>text</p></blockquote>

the same idea should work in a case if I try to apply the code_block on the part of the blockquote block

<p>some</p>
<blockquote><p>text</p></blockquote> // select "xt" and apply the code_block format
->
<p>some</p>
<blockquote><p>te</p></blockquote>
<pre><code>xt</code></pre>

I tried to implement this using different strategies. The first one is

const node = state.schema.nodes[nodeType.name].create(null, state.selection.content().content);
tr.replaceSelectionWith(node)
  .setSelection(TextSelection.create(tr.doc, tr.selection.to - 2))
  .scrollIntoView();
dispatch(tr);

But there is possibility that schema would be broken. The second one is

    if (selection.$from.parentOffset > 0) {
        tr.split(tr.mapping.map(selection.from));
    }
    if (selection.$to.parentOffset < selection.$to.node().content.size) {
        tr.split(tr.mapping.map(selection.to));
    }
    tr.setSelection(
        TextSelection.create(tr.doc, tr.selection.$to.pos - tr.selection.$to.depth - 1)
    );
    dispatch(tr);
    wrapIn(nodeType)(state, dispatch, view);

In the first, I split selection by paragraphs and then use wrapIn. But, unfortunately, it works unpredictable in some cases.

I understand that my solution should have a few steps:

  1. Check selection, if there are some blocks like blockquote or code_block, I should analyse them
  2. During analysis I should cut inline parts of the node and close it
  3. Combine all of them and apply format

Am I right? Do you have any corrections or ideas?

Just an FYI of this semi-related issue (no solution): Lift blockquote instead of list? - #2 by prosed

@alexaaaant I think your algorithm is right. If you want to change a type of part of inlined content, you should split a parent node into new nodes and change types for them according to your wishes. And yes, it’s not so easy as sounds because you should be careful with positions to prevent broken schemas.