Stackoverflow when deleting lines

Hello, I’m having a bug in my editor that I didn’t notice before, and I have no clue for now about what I’m doing wrong.

The bug scenario is about deleting more than 1 line (1 line is fine) by selecting + hitting the backspace key. This calls replaceStep then fitRight then fitRightJoin, then the recursivity starts with

  1. fillBefore()
  2. search()
  3. search()
  4. anonymous()
  5. createAndFill() which loops to 1 because of the call var after = this.contentMatch.matchFragment(content).fillBefore(Fragment.empty, true);

The error finally displays:

RangeError: Maximum call stack size exceeded
at NodeType.createAndFill (index.es.js:1971)
at index.es.js:1507
at Array.map (<anonymous>)
at search (index.es.js:1507)
at search (index.es.js:1513)
at ContentMatch.fillBefore (index.es.js:1519)
at NodeType.createAndFill (index.es.js:1979)
at index.es.js:1507
at Array.map (<anonymous>)
at search (index.es.js:1507)

Any idea of what could cause this? I try to see if some of my plugins are called but, looking at the stack and breakpoints set, it seems not.

Can you show the content expression of the schema nodes that are involved in this? (Probably the top node and whatever kind of block nodes you are deleting.)

Yes probably, indeed it occurs only when I try to delete all the lines, but I don’t understand why it works fine when deleting 1 single line.

Here is the “after” content node at fillBefore() time:

The schema is as below (so I think only the simpleAnswer part is relevant here):

nodes: {
    doc: {content: 'answer+'},
    simpleAnswer: {
      TAG: 'div',
      toDOM(node) {
        return [node.type.spec.TAG, {class: 'simple'}, 0];
      },
      parseDOM: [{tag: 'div[class="simple"]'}],
      group: 'answer',
      content: 'textBlock+',
    },
    switchAnswer: {
      TAG: 'section',
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
      parseDOM: [{tag: 'section'}],
      group: 'answer',
      content: 'switchCase+',
    },
    switchCase: {
      TAG: 'article',
      attrs: {case: {default: 'default'}, timestamp: {default: undefined}},
      toDOM(node) {
        return [node.type.spec.TAG, {case: node.attrs.case}, 0];
      },
      parseDOM: [{tag: 'article'}],
      content: 'answer+',
    },

Thank you.

How is textBlock defined? Are there any loops in the schema where, if you take the first element from a group used in node N’s content, you directly or indirectly end up with an N node again?

textBlock is a group name. The simpleAnswer's content is defined as one or more textBlocks, and several nodes (paragraph, lists) are declared to be part of the textBlock group.

There is another answer group, that allows to nest any answers (simpleAnswer or switchAnswer) into switchCases of switchAnswers.

Sorry that I didn’t included the full schema:

  nodes: {
    doc: {content: 'answer+'},
    switchAnswer: {
      TAG: 'section',
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
      parseDOM: [{tag: 'section'}],
      group: 'answer',
      content: 'switchCase+',
    },
    switchCase: {
      TAG: 'article',
      attrs: {case: {default: 'default'}, timestamp: {default: undefined}},
      toDOM(node) {
        return [node.type.spec.TAG, {case: node.attrs.case}, 0];
      },
      parseDOM: [{tag: 'article'}],
      content: 'answer+',
    },
    simpleAnswer: {
      TAG: 'div',
      toDOM(node) {
        return [node.type.spec.TAG, {class: 'simple'}, 0];
      },
      parseDOM: [{tag: 'div[class="simple"]'}],
      group: 'answer',
      content: 'textBlock+',
    },
    paragraph: {
      TAG: 'p',
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
      parseDOM: [{tag: 'p'}],
      group: 'textBlock',
      content: 'text*',
      marks: '_', // any mark
    },
    unorderedList: {
      TAG: 'ul',
      content: 'listItem+',
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
      group: 'textBlock',
      parseDOM: [{tag: 'ul'}],
    },
    orderedList: {
      TAG: 'ol',
      content: 'listItem+',
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
      group: 'textBlock',
      parseDOM: [{tag: 'ol'}],
    },
    listItem: {
      TAG: 'li',
      content: 'text*',
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
      parseDOM: [{tag: 'li'}],
    },
    text: {inline: true},
    image: {
      TAG: 'img',
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
      parseDOM: [{tag: 'img'}],
      inlineContent: true,
    },
  },
  marks: {
    bold: {
      TAG: 'b',
      parseDOM: [
        {
          tag: 'b',
          getAttrs(node: any) {
            return node.style && node.style.fontWeight !== 'normal' && null;
          },
        },
        {tag: 'strong'},
        {
          style: 'font-weight',
          getAttrs(value) {
            return typeof value === 'string' && /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null;
          },
        },
      ],
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
    },
    italic: {
      TAG: 'em',
      parseDOM: [{tag: 'i'}, {tag: 'em'}, {style: 'font-style=italic'}],
      toDOM(node) {
        return [node.type.spec.TAG, 0];
      },
    },
    link: {
      TAG: 'a',
      parseDOM: [
        {
          tag: 'a[href]',
          getAttrs(dom) {
            if (dom instanceof Element) {
              return {
                href: dom.getAttribute('href'),
                title: dom.getAttribute('title'),
                target: {default: '_blank'},
              };
            } else {
              return false;
            }
          },
        },
      ],
      attrs: {href: {}, target: {default: '_blank'}},
      toDOM(node) {
        return [node.type.spec.TAG, {href: node.attrs.href, title: node.attrs.href, target: node.attrs.target}, 0];
      },
    },
  },

Basically the doc is a set of one or more answers, and those answers can be simple (paragraphs) or switches. When switches, the cases of those switches are other answers, that can be either simple or (nested) switches themselves.

I think moving simpleAnswer above switchAnswer in your schema should help with this. Right now, since switchAnswer is the default answer node, the library keeps trying to create those, but then as it generates its minimum valid content, it has to create another answer, and so on.

1 Like

Yes that fixes it :slight_smile: I didn’t know about the “defaut” node for a group. Thanks a lot for the tip!