You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CKEditor5多行粘贴操作撤销时触发model-position-path-incorrect-format错误

CKEditor5插件多行内容撤销报错:model-position-path-incorrect-format

问题背景

我基于CKEditor5开发了一款插件,用于将粘贴内容中用()、$$等分隔符包裹的公式,转换为ckeditor5-math兼容的数学公式。为此修改了AutoMath插件以支持这类分隔符,但遇到了撤销异常:单行内容的Ctrl+Z操作正常,多行内容执行两次撤销后会抛出错误。

复现步骤

为定位问题,我做了一个无数学插件依赖的测试版本(将&包裹的文本转为粗体):

  • 将光标置于单词内部(非文本首尾,暂未明确原因)
  • 粘贴以下内容:
aa &bb& cc
dd

ee &ff& gg
  • 执行两次撤销操作,触发报错。

错误信息

Uncaught CKEditorError: model-position-path-incorrect-format {"path":[]}

问题分析

已知错误与操作批次有关,大概率涉及模型位置的父元素缓存失效,但尚未明确具体实现层面的问题。核心疑点包括:

  1. 延迟执行的转换操作与粘贴操作不在同一undo批次,导致撤销时位置跟踪错乱
  2. LiveRange/LivePosition在模型结构变化后失效,仍被后续操作引用
  3. 批量修改模型时,TextProxy对象失效引发位置路径错误

测试插件修复版代码

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
import LivePosition from '@ckeditor/ckeditor5-engine/src/model/liveposition';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';

export default class Test extends Plugin {
    static get requires() {
        return [Undo];
    }

    static get pluginName() {
        return 'Test';
    }

    constructor(editor) {
        super(editor);

        this._timeoutId = null;
        this._equationRange = null; // 保存当前处理的LiveRange,用于撤销时清理
    }

    init() {
        const editor = this.editor;
        const modelDocument = editor.model.document;

        this.listenTo(editor.plugins.get('ClipboardPipeline'), 'inputTransformation', (evt, data) => {
            const firstRange = modelDocument.selection.getFirstRange();

            const leftLivePosition = LivePosition.fromPosition(firstRange.start);
            leftLivePosition.stickiness = 'toPrevious';

            const rightLivePosition = LivePosition.fromPosition(firstRange.end);
            rightLivePosition.stickiness = 'toNext';

            modelDocument.once('change:data', () => {
                this._boldBetweenPositions(leftLivePosition, rightLivePosition);

                leftLivePosition.detach();
                rightLivePosition.detach();
            }, {priority: 'high'});
        });

        editor.commands.get('undo').on('execute', () => {
            if (this._timeoutId) {
                global.window.clearTimeout(this._timeoutId);
                this._timeoutId = null;
            }
            // 撤销时清理失效的LiveRange,避免引用无效位置
            if (this._equationRange) {
                this._equationRange.detach();
                this._equationRange = null;
            }
        }, {priority: 'high'});
    }


    _boldBetweenPositions(leftPosition, rightPosition) {
        const editor = this.editor;
        this._equationRange = new LiveRange(leftPosition, rightPosition);

        // 延迟执行,允许用户撤销转换操作
        this._timeoutId = global.window.setTimeout(() => {
            this._timeoutId = null;

            // 检查范围是否仍附着在模型中,跳过无效操作
            if (!this._equationRange.isAttached()) {
                this._equationRange.detach();
                this._equationRange = null;
                return;
            }

            editor.model.change(writer => {
                // 先收集所有待修改内容,避免修改模型后Walker失效
                const pendingChanges = [];
                const walker = this._equationRange.getWalker({ignoreElementEnd: true});
                
                for (const node of walker) {
                    const text = node.item.data;
                    if (node.item.is('$textProxy') && text?.match(/&/g)) {
                        pendingChanges.push({
                            range: writer.createRange(node.previousPosition, node.nextPosition),
                            content: text
                        });
                    }
                }

                // 反向执行修改,避免位置偏移
                for (let i = pendingChanges.length - 1; i >= 0; i--) {
                    const { range, content } = pendingChanges[i];
                    writer.remove(range);
                    
                    const splitParts = this._split(content);
                    for (let j = splitParts.length - 1; j >= 0; j--) {
                        if (j % 2 === 0) {
                            writer.insertText(splitParts[j], range.start);
                        } else {
                            writer.insertText(splitParts[j], {bold: true}, range.start);
                        }
                    }
                }
            });

            this._equationRange.detach();
            this._equationRange = null;
        }, 100);
    }

    // 修复分割逻辑,正确提取普通文本与需要加粗的内容
    _split(text) {
        const parts = text.split(/&([^&]+)&/g);
        return parts.filter(part => part); // 过滤空字符串
    }
}

修复说明

  1. LiveRange生命周期管理:新增_equationRange实例变量,在撤销时主动清理失效范围,避免引用已被删除的模型位置
  2. 有效性前置检查:延迟执行转换前,先判断LiveRange是否仍附着在模型中,跳过无效操作
  3. 批量修改优化:先收集所有待修改的内容和范围,再反向执行修改,避免模型结构变化导致的位置偏移
  4. 分割逻辑修复:修正_split方法,确保正确提取普通文本与需要加粗的片段,避免内容丢失

内容的提问来源于stack exchange,提问作者kinda_sus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 00:00:55