Files

100 lines
3.8 KiB
JavaScript

'use client';
import { Mark } from '@tiptap/core';
/**
* GhostMark — Tiptap Mark 扩展
* AI 生成的文本标记为 ghost-text,以半透明样式显示。
* 用户接受时去除 mark,拒绝时删除文本。
*/
const GhostMark = Mark.create({
name: 'ghostText',
// 不会被持久化到 HTML(保存时自动剥离)
addOptions() {
return {
HTMLAttributes: {
class: 'ghost-text',
},
};
},
parseHTML() {
return [
{
tag: 'span.ghost-text',
},
];
},
renderHTML({ HTMLAttributes }) {
return ['span', { ...this.options.HTMLAttributes, ...HTMLAttributes }, 0];
},
addCommands() {
return {
setGhostText: () => ({ commands }) => {
return commands.setMark(this.name);
},
unsetGhostText: () => ({ commands }) => {
return commands.unsetMark(this.name);
},
// 删除文档中所有 ghost 标记(保留文本,去掉半透明)
acceptAllGhost: () => ({ tr, dispatch }) => {
if (dispatch) {
tr.doc.descendants((node, pos) => {
if (node.isText) {
const ghostMark = node.marks.find(m => m.type.name === this.name);
if (ghostMark) {
tr.removeMark(pos, pos + node.nodeSize, ghostMark.type);
}
}
});
}
return true;
},
// 删除带 ghost 标记的所有文本节点,并清理残留的空段落
// ghostStart: 可选,ghost 文本插入起始位置,仅清理该位置之后的空段落
removeAllGhost: (ghostStart) => ({ tr, dispatch }) => {
if (dispatch) {
// 第一步:收集所有 ghost 文本范围
const ranges = [];
tr.doc.descendants((node, pos) => {
if (node.isText) {
const ghostMark = node.marks.find(m => m.type.name === this.name);
if (ghostMark) {
ranges.push({ from: pos, to: pos + node.nodeSize });
}
}
});
// 记录 ghost 区域的最小起始位置(用于限定空段落清理范围)
const effectiveStart = ghostStart != null
? ghostStart
: (ranges.length > 0 ? ranges[0].from : null);
// 从后向前删除 ghost 文本
for (let i = ranges.length - 1; i >= 0; i--) {
tr.delete(ranges[i].from, ranges[i].to);
}
// 第二步:清理 ghost 区域内因删除文本而产生的空段落
if (effectiveStart != null) {
const emptyParas = [];
tr.doc.descendants((node, pos) => {
if (node.type.name === 'paragraph' && node.content.size === 0 && pos >= effectiveStart) {
emptyParas.push({ from: pos, to: pos + node.nodeSize });
}
});
for (let i = emptyParas.length - 1; i >= 0; i--) {
if (tr.doc.content.childCount > 1) {
tr.delete(emptyParas[i].from, emptyParas[i].to);
}
}
}
}
return true;
},
};
},
});
export default GhostMark;