Off-By-One `RangeError` Crash and Text Duplication in `StyleManager.editLink` and `deleteLink`
还没有人认领这个 Issue。
评估
- 难度
- 2/5
- 预计耗时
- 1-3 小时
- 新手友好度
- 78/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- typescript
- 领域
- frontend
调研方向
从 packages/core/src/editor/managers/StyleManager.ts 开始,重点关注所引用行附近的 getLinkMarkAtPos、editLink 和 deleteLink。复现文档末尾和末尾链接这两种情况,然后验证边界位置不会抛出异常,编辑会替换链接文本且不会重复,以及删除会移除链接标记。
由索引模型根据 Issue 内容生成。
描述
What’s broken?
In @blocknote/core, StyleManager.editLink and StyleManager.deleteLink perform an unconditional position + 1 lookup when locating the link mark at the current cursor position:
Inside packages/core/src/editor/managers/StyleManager.ts:220-260:
ts
public editLink(
url: string,
text: string,
position = this.editor.transact((tr) => tr.selection.anchor),
) {
this.editor.transact((tr) => {
const linkData = this.getLinkMarkAtPos(position + 1);
const { from, to } = linkData || {
from: tr.selection.from,
to: tr.selection.to,
};
and in deleteLink:
public deleteLink(
position = this.editor.transact((tr) => tr.selection.anchor),
) {
this.editor.transact((tr) => {
const linkData = this.getLinkMarkAtPos(position + 1);
const { from, to } = linkData || {
from: tr.selection.from,
to: tr.selection.to,
};
//
This causes two major issues:
- Unhandled RangeError Crash: If the link is positioned at the very end of the document (position === tr.doc.content.size), position + 1 resolves beyond document bounds. Inside getLinkMarkAtPos, tr.doc.resolve(pos) throws an uncaught RangeError: Position out of range, causing the editor to crash.
- Text Duplication & Delete Failure: When the cursor caret is resting at the end of a link (position === link.to), position + 1 queries the character after the link. Since that node lacks the link mark, getLinkMarkAtPos returns undefined and falls back to { from: tr.selection.from, to: tr.selection.to }. Because the selection is a collapsed caret (from === to), editLink inserts the new text beside the old text without replacing it (resulting in OriginalTextEditedText duplication), and deleteLink attempts tr.removeMark(from, to, link) over an empty 0-length range, completely failing to remove the link.
What did you expect to happen?
- editLink and deleteLink should never throw an out-of-range error when the cursor is at the end of a document.
- When the cursor is positioned at the boundary or end of a link (position === link.to), editLink should successfully replace the existing link text rather than duplicating it, and deleteLink should cleanly remove the link mark.
Steps to reproduce
Scenario A: RangeError Crash
- Create a document where a link is the last element: GitHub
- Place the cursor at the end of the document (position === editor.prosemirrorState.doc.content.size).
- Trigger editor.editLink("https://github.com", "GitHub Homepage") or editor.deleteLink().
- Observed: Uncaught RangeError: Position out of range thrown from tr.doc.resolve(pos).
Scenario B: Text Duplication / Failed Deletion
- Type a link: Google and place the cursor at the end of the text (position === link.to).
- Call editor.editLink("https://google.com", "Google Search").
- Observed: The text becomes GoogleGoogle Search instead of Google Search.
- Call editor.deleteLink() with the cursor at the same position.
- Observed: The link remains active and is not deleted.
BlockNote version
Version: 0.54.0 (and main branch) Package: @blocknote/core
Environment
OS: Any (Windows / macOS / Linux) Browsers: Chrome, Firefox, Safari, Edge Frameworks: Vanilla JS, React, Vue
Additional context
Root Cause
getLinkMarkAtPos expects a valid in-bounds position. Unconditionally adding + 1 breaks on right-boundary cursor positions and document ends.
Proposed Fix
- Guard getLinkMarkAtPos against positions exceeding tr.doc.content.size.
- Inspect position directly, falling back to position - 1 if the cursor is at the trailing boundary of the link:
--- a/packages/core/src/editor/managers/StyleManager.ts
+++ b/packages/core/src/editor/managers/StyleManager.ts
@@ -154,6 +154,10 @@ export class StyleManager {
return this.editor.transact((tr) => {
+ const clampedPos = Math.min(Math.max(0, pos), tr.doc.content.size);
+ const resolvedPos = tr.doc.resolve(clampedPos);
const linkMark = resolvedPos
.marks()
.find((mark) => mark.type.name === "link");
@@ -224,7 +228,8 @@ export class StyleManager {
this.editor.transact((tr) => {
- const linkData = this.getLinkMarkAtPos(position + 1);
+ const linkData =
+ this.getLinkMarkAtPos(position) ||
+ (position > 0 ? this.getLinkMarkAtPos(position - 1) : undefined);
const { from, to } = linkData || {
from: tr.selection.from,
to: tr.selection.to,
};
@@ -248,7 +253,8 @@ export class StyleManager {
this.editor.transact((tr) => {
- const linkData = this.getLinkMarkAtPos(position + 1);
+ const linkData =
+ this.getLinkMarkAtPos(position) ||
+ (position > 0 ? this.getLinkMarkAtPos(position - 1) : undefined);
const { from, to } = linkData || {
from: tr.selection.from,
to: tr.selection.to,
Contribution
- I'd be interested in contributing a fix for this issue
Sponsor
- I'm a sponsor and would appreciate if you could look into this sooner than later 💖
- 主要语言
- TypeScript
- 星标
- 10.2k
- 派生
- 772
- 平均合并
- 3 天 11 小时
- 30 天内合并 PR
- 17
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
TypeCellOS/BlockNote 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 84/100
TypeCellOS/BlockNote#3098 ·
-
needs-triage
难度 2/5 1-3 小时 新手友好度 85/100
TypeCellOS/BlockNote#3072 ·
-
难度 2/5 1-3 小时 新手友好度 82/100
TypeCellOS/BlockNote#2949 · 1 条评论 ·
-
a11y
难度 2/5 1-3 小时 新手友好度 68/100
TypeCellOS/BlockNote#2855 ·
-
a11y
难度 2/5 1-3 小时 新手友好度 62/100
TypeCellOS/BlockNote#2829 · 1 条评论 ·
查看 TypeCellOS/BlockNote 的全部 Issue
相似的 Issue
-
Type/Bug
难度 2/5 1-3 小时 新手友好度 78/100
OpenNSW/nsw-srilanka#497 ·
-
难度 2/5 1-3 小时 新手友好度 72/100
0xMiden/bridge-portal#132 ·
-
react-doctor severity:warning tech-debt
难度 1/5 1 小时以内 新手友好度 88/100
digidem/comapeo-cloud-app#403 ·
-
难度 2/5 1-3 小时 新手友好度 72/100
-
难度 2/5 1-3 小时 新手友好度 78/100