Skip to content

refactor: simplify @tiptap/extension-link by inlining it to BlockNote#2623

Open
nperez0111 wants to merge 6 commits intomainfrom
feat/inline-tiptap-link
Open

refactor: simplify @tiptap/extension-link by inlining it to BlockNote#2623
nperez0111 wants to merge 6 commits intomainfrom
feat/inline-tiptap-link

Conversation

@nperez0111
Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 commented Apr 3, 2026

  • refactor: remove dep on @tiptap/extension-link and linkify
  • refactor: simplify inlined Link extension by removing unused tiptap options

Summary

The goal of this change is that @tiptap/extension-link has proved to be somewhat problematic in the past for a couple of reasons:

  • It depends on linkify, which is a fairly heavy dep since it ships a whole parser & state machine for link detection. Which has awkward initialization semantics
  • The link extension is very configurable in Tiptap since there are many different use cases, which we do not expose at the BlockNote level

Rationale

Given these, I felt it was appropriate to just inline the extension into BlockNote & write a simpler regex based matcher for link detection. It will not be quite as full featured as linkify was, but it is an order of magnitude simpler & lighter.

Changes

  • Inlines @tiptap/extension-link into BlockNote core. Simplfies interface since we don't need all the configuration options it had
  • Removes dependency on linkify since it was a heavyweight dep, and somewhat inappropriate for this use-case anyway

Impact

  • This can possibly lead to more things being linked that weren't before, or something not being linked that would have before. This is mitigated by test cases that were taken based on the existing linkify behaviors.
  • Adds some complexity & maintenance that is our responsibility now, at the trade off of more control over the package to make changes

Testing

  • Added tests to understand the current linkfiy behavior, and built an algo to mimic the same behaviors that were captured there.

Screenshots/Video

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

Summary by CodeRabbit

  • New Features

    • Robust in-editor autolinking while typing, improved URL/email detection/tokenization, paste-to-link, and click-to-open link behavior
    • New Link mark with href/target/rel handling and safer URI validation
    • Editor link API: get link at position, edit link in-place, delete link
  • Tests

    • Comprehensive unit and integration tests covering detection, tokenization, autolink, paste handling, and editor flows
  • Chores

    • Removed external link extension dependency and updated Dependabot configuration

nperez0111 and others added 2 commits April 2, 2026 18:59
…ptions

Strip out carried-over options (openOnClick, enableClickSelection, linkOnPaste,
protocols, validate), deprecated types (LinkProtocolOptions, LinkOptions), and
verbose JSDoc comments. Inline configuration defaults directly, pre-compile the
URI validation regex, and simplify the extension registration in ExtensionManager.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Apr 4, 2026 10:24am
blocknote-website Error Error Apr 4, 2026 10:24am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f9c502c-d2ce-4a08-b252-8ae07922c6b6

📥 Commits

Reviewing files that changed from the base of the PR and between 1e7e92c and 28b2a44.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • docs/package.json
✅ Files skipped from review due to trivial changes (1)
  • docs/package.json

📝 Walkthrough

Walkthrough

Replaces external @tiptap/extension-link with a vendored Link mark; removes the dependency and Dependabot allow-list entry; adds link detection/tokenization, autolink, paste and click handler plugins, editor/style-manager link APIs, tests, and node-conversion refactors.

Changes

Cohort / File(s) Summary
Dependency Removal
\.github/dependabot.yml, packages/core/package.json
Removed @tiptap/extension-link from Dependabot allow-list and package dependencies.
Extension Manager
packages/core/src/editor/managers/ExtensionManager/extensions.ts
Switched from external TipTap Link extension to vendored .../Link/link.js; removed prior protocol/linkify init and custom attribute filtering.
Vendored Link Extension
packages/core/src/extensions/tiptap-extensions/Link/link.ts, packages/core/src/extensions/tiptap-extensions/Link/index.ts, .../Link/link.js
Added vendored Link mark (attrs: href,target,rel), isAllowedUri, paste rule and registrations for autolink, click, and paste plugins.
Autolink Plugin
packages/core/src/extensions/tiptap-extensions/Link/helpers/autolink.ts
New ProseMirror plugin factory using appendTransaction to detect changed ranges, tokenize trailing words, validate via provided hooks, and add link marks; respects preventAutolink.
Paste Handler
packages/core/src/extensions/tiptap-extensions/Link/helpers/pasteHandler.ts
New paste plugin that detects full-slice URLs and applies link marks via editor command when selection is non-empty and shouldAutoLink permits.
Click Handler
packages/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts
New click plugin intercepting left-clicks on anchors within editor root, merging DOM and mark attrs, and opening links via window.open.
Link Detection & Tokenization
packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts
New findLinks() and tokenizeLink() implementing protocol/mailto/email/schemeless detection with TLD checks, trimming punctuation/brackets, and producing tokens for autolink.
Whitespace Helpers
packages/core/src/extensions/tiptap-extensions/Link/helpers/whitespace.ts
Added Unicode whitespace regex constants used by tokenization and autolink logic.
Tests
packages/core/src/extensions/tiptap-extensions/Link/link.test.ts
Comprehensive Vitest suite covering detection, tokenization, autolink integration, and paste handler behavior (positive and negative cases).
Editor & Style Manager APIs
packages/core/src/editor/BlockNoteEditor.ts, packages/core/src/editor/managers/StyleManager.ts
Added getLinkMarkAtPos, editLink, deleteLink on editor; StyleManager gains link-range resolution, edit/delete implementations, and updated getSelectedLinkUrl.
Link Toolbar
packages/core/src/extensions/LinkToolbar/LinkToolbar.ts
Refactored to use editor.getLinkMarkAtPos; delegated edit/delete to editor methods and removed direct mark-range/schema logic.
Node Conversions
packages/core/src/api/nodeConversions/blockToNode.ts, packages/core/src/api/nodeConversions/nodeToBlock.ts
linkToNodes restricted to text nodes; contentNodeToInlineContent refactored to a two-pass flatten+merge approach with extractMarks helper and adjusted hard-break handling.
Docs
docs/package.json
Pinned better-auth to patch updates by changing caret to tilde (^1.4.15~1.4.15).

Sequence Diagrams

sequenceDiagram
    participant Editor as Editor
    participant ProseMirror as ProseMirror
    participant Autolink as Autolink Plugin
    participant LinkDetector as Link Detector
    participant Schema as Schema/Marks

    Note over Editor,ProseMirror: Autolink flow
    Editor->>ProseMirror: commit transactions (text change)
    ProseMirror->>Autolink: appendTransaction(transactions, oldState, newState)
    Autolink->>Autolink: check preventAutolink meta & doc diff
    Autolink->>LinkDetector: tokenizeLink(textSegment, defaultProtocol)
    LinkDetector-->>Autolink: LinkMatch tokens
    Autolink->>Schema: verify no code mark / existing link mark
    Autolink->>Autolink: run validate() and shouldAutoLink()
    Autolink->>ProseMirror: tr.addMark(from, to, linkMark)
    Autolink-->>ProseMirror: return transaction (if modified)
Loading
sequenceDiagram
    participant User as User
    participant EditorDOM as Editor (DOM)
    participant PastePlugin as Paste Handler
    participant LinkDetector as Link Detector
    participant Commands as Editor Commands

    Note over User,Commands: Paste handling
    User->>EditorDOM: paste event (selection non-empty)
    EditorDOM->>PastePlugin: handlePaste(event, slice)
    PastePlugin->>PastePlugin: extract slice textContent
    PastePlugin->>LinkDetector: findLinks(text, defaultProtocol)
    LinkDetector-->>PastePlugin: LinkMatch[]
    PastePlugin->>PastePlugin: verify full-match && shouldAutoLink
    PastePlugin->>Commands: setMark(type, { href })
    Commands-->>EditorDOM: apply link mark
Loading
sequenceDiagram
    participant User as User
    participant EditorDOM as Editor (DOM)
    participant ClickPlugin as Click Handler
    participant Schema as Marks
    participant External as External URL

    Note over User,External: Click handling
    User->>EditorDOM: left-click on link element
    EditorDOM->>ClickPlugin: handleClick(view, pos, event)
    ClickPlugin->>ClickPlugin: find closest <a> within editor root
    ClickPlugin->>Schema: get mark attrs at pos
    ClickPlugin->>ClickPlugin: merge attrs, resolve href & target
    alt href present
        ClickPlugin->>External: window.open(href, target)
        ClickPlugin-->>EditorDOM: return true
    else
        ClickPlugin-->>EditorDOM: return false
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Vendored links hop into place,
Autolinks chase whitespace with grace,
Paste and click give gentle taps,
Editors edit, marks unwrap,
A rabbit cheers this tidy patch.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main refactoring: removing the external @tiptap/extension-link dependency and inlining it into BlockNote.
Description check ✅ Passed The description covers all key template sections with relevant details: a clear summary of the refactoring goal, comprehensive rationale for the change, specific changes made, potential impacts on linking behavior, testing approach, and code standards checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/inline-tiptap-link

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
packages/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts (1)

49-55: Validate the clicked URI in clickHandler for defense-in-depth.

While parseHTML() validates URIs before creating marks and linkDetector filters for safe protocols, the window.open(href, target) call at line 54 still receives untrusted input from the DOM element's href property without explicit validation. Add a check using isAllowedUri() before opening to prevent any edge case where an invalid URI could reach this handler. Also, add noopener,noreferrer when opening a new tab, as the link mark's rel attribute doesn't apply to window.open() calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts`
around lines 49 - 55, The click handler currently calls window.open(href,
target) with unvalidated href; update clickHandler to first run
isAllowedUri(href) (use the same isAllowedUri helper used elsewhere) and return
false if it fails, then call window.open only for allowed URIs; also ensure you
pass the noopener,noreferrer feature string (e.g. window.open(href, target,
'noopener,noreferrer')) when opening a new tab so the rel attribute semantics
are preserved for window.open calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/extensions/tiptap-extensions/Link/helpers/autolink.ts`:
- Around line 22-29: isValidLinkStructure currently only accepts a single link
token or a 3-token wrapper like () or [], but tokenizeLink/linkDetector can also
emit a 2-token form like [link, "."] (e.g., "example.com."). Update
isValidLinkStructure to also return true when tokens.length === 2 and the first
token is a link and the second token is a trailing punctuation dot
(tokens[0].isLink && tokens[1].value === ".") while keeping the existing
single-token and 3-token ("()", "[]") logic so the validator stays in sync with
tokenizeLink()/linkDetector behavior.

In `@packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts`:
- Around line 33-35: The autolink and paste paths are inconsistent because only
isSingleUrl() knows about SPECIAL_HOSTS and SCHEMELESS_RE enforces a dotted host
so hosts like "localhost" (and with port/path like "localhost:3000" or
"localhost/foo") are never recognized; update the link detection logic so both
tokenizeLink() and findLinks() accept SPECIAL_HOSTS by: expanding SCHEMELESS_RE
(or adding a parallel regex) to allow hostname-only hosts and optional
:port/path, and centralizing the special-host check used by isSingleUrl() into
the shared linkDetector helpers (e.g., reference SPECIAL_HOSTS, isSingleUrl,
SCHEMELESS_RE, tokenizeLink, findLinks) so pasted and typed links consistently
detect localhost variants.
- Around line 56-57: The current SCHEMELESS_RE (and the similar regex used later
in tokenizeLink/findLinks) only allows an optional suffix that begins with "/"
so URLs like example.com?x=1 or example.com#frag are rejected or truncated;
update the regexes (SCHEMELESS_RE and the other regex referenced around
tokenizeLink/findLinks) to allow the suffix to start with "/", "?" or "#" (e.g.,
change the suffix-start character class from "/" to "[\/?#]" and keep the rest
of the suffix pattern [^\s]* and existing port handling) so schemeless URLs with
query or fragment parts are matched correctly.
- Around line 381-386: linkToken() currently classifies any string containing
"@" and not "://" as an email, which causes buildHref() to prepend mailto: to
already-prefixed values (e.g. mailto:user@example.com → mailto:mailto:...), so
update the classification logic in linkToken() (or the snippet that sets const
type) to detect existing mailto prefixes (use a case-insensitive check like
value.toLowerCase().startsWith("mailto:") or /^mailto:/i) and treat those as
already-hrefed (do not reclassify or re-prefix); alternatively, ensure
buildHref() checks for an existing mailto: prefix before adding one—refer to
isSingleUrl(), linkToken(), buildHref(), and defaultProtocol when making the
change.

In `@packages/core/src/extensions/tiptap-extensions/Link/link.test.ts`:
- Around line 822-855: The test currently guards the paste plugin call with an
if so assertions may be skipped; instead assert the plugin and its handler exist
before invoking: locate the test "does not apply link when pasting URL with
empty selection" and replace the conditional block that finds pastePlugin with
explicit expectations (e.g., expect(pastePlugin).toBeDefined() and
expect(pastePlugin.props.handlePaste).toBeDefined()), then call
pastePlugin.props.handlePaste(...) and assert the returned value is false; keep
references to createEditor, editor.replaceBlocks, editor.setTextCursorPosition,
and pastePlugin.props.handlePaste to find and update the code.
- Around line 710-766: The test is unreliable because it conditionally skips
assertions when pastePlugin, pastePlugin.props.handlePaste, or result are falsy;
change the test to assert those values exist instead of guarding: import
TextSelection, Slice, Fragment at top (replace the two require calls) and then
assert pastePlugin is defined and pastePlugin.props.handlePaste is a function
(e.g., expect(pastePlugin).toBeDefined(); expect(typeof
pastePlugin.props.handlePaste).toBe("function")), call the handler and assert
result is truthy (expect(result).toBeTruthy()), and finally assert
getLinksInDocument(editor) yields the expected link (href and text) so the test
fails if the plugin or handler is missing or returns falsy.
- Around line 769-820: The test currently guards the assertions behind "if
(pastePlugin && pastePlugin.props.handlePaste)" which lets the test silently
pass when the plugin or handler is missing; update the test to assert the plugin
and handler exist before invoking them: replace the conditional with explicit
expectations like expect(pastePlugin).toBeDefined() and
expect(pastePlugin.props.handlePaste).toBeDefined(), then call
(pastePlugin.props.handlePaste as any)(...) and run the existing
expect(result).toBe(false) and
expect(getLinksInDocument(editor)).toHaveLength(0); reference the pastePlugin
variable and its props.handlePaste, the ClipboardEvent invocation, and
getLinksInDocument to locate the changes.

---

Nitpick comments:
In `@packages/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts`:
- Around line 49-55: The click handler currently calls window.open(href, target)
with unvalidated href; update clickHandler to first run isAllowedUri(href) (use
the same isAllowedUri helper used elsewhere) and return false if it fails, then
call window.open only for allowed URIs; also ensure you pass the
noopener,noreferrer feature string (e.g. window.open(href, target,
'noopener,noreferrer')) when opening a new tab so the rel attribute semantics
are preserved for window.open calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5978724-8a30-44e2-8e0b-36d56c1322e5

📥 Commits

Reviewing files that changed from the base of the PR and between bda3045 and 989fb85.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .github/dependabot.yml
  • packages/core/package.json
  • packages/core/src/editor/managers/ExtensionManager/extensions.ts
  • packages/core/src/extensions/tiptap-extensions/Link/helpers/autolink.ts
  • packages/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts
  • packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts
  • packages/core/src/extensions/tiptap-extensions/Link/helpers/pasteHandler.ts
  • packages/core/src/extensions/tiptap-extensions/Link/helpers/whitespace.ts
  • packages/core/src/extensions/tiptap-extensions/Link/index.ts
  • packages/core/src/extensions/tiptap-extensions/Link/link.test.ts
  • packages/core/src/extensions/tiptap-extensions/Link/link.ts
💤 Files with no reviewable changes (1)
  • packages/core/package.json

Comment on lines +22 to +29
function isValidLinkStructure(tokens: LinkMatch[]) {
if (tokens.length === 1) {
return tokens[0].isLink;
}

if (tokens.length === 3 && tokens[1].isLink) {
return ["()", "[]"].includes(tokens[0].value + tokens[2].value);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep isValidLinkStructure() in sync with tokenizeLink().

packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts:302-329 can emit {link} and [link, "."], but this validator still only accepts a bare link or a 3-token () / [] wrapper. Typing example.com. followed by whitespace therefore never autolinks, even though the tokenizer has explicit support for that case.

Suggested fix
 function isValidLinkStructure(tokens: LinkMatch[]) {
   if (tokens.length === 1) {
     return tokens[0].isLink;
   }
 
+  if (tokens.length === 2) {
+    return tokens[0].isLink && tokens[1].value === ".";
+  }
+
   if (tokens.length === 3 && tokens[1].isLink) {
-    return ["()", "[]"].includes(tokens[0].value + tokens[2].value);
+    return ["()", "[]", "{}"].includes(tokens[0].value + tokens[2].value);
   }
 
   return false;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isValidLinkStructure(tokens: LinkMatch[]) {
if (tokens.length === 1) {
return tokens[0].isLink;
}
if (tokens.length === 3 && tokens[1].isLink) {
return ["()", "[]"].includes(tokens[0].value + tokens[2].value);
}
function isValidLinkStructure(tokens: LinkMatch[]) {
if (tokens.length === 1) {
return tokens[0].isLink;
}
if (tokens.length === 2) {
return tokens[0].isLink && tokens[1].value === ".";
}
if (tokens.length === 3 && tokens[1].isLink) {
return ["()", "[]", "{}"].includes(tokens[0].value + tokens[2].value);
}
return false;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/tiptap-extensions/Link/helpers/autolink.ts`
around lines 22 - 29, isValidLinkStructure currently only accepts a single link
token or a 3-token wrapper like () or [], but tokenizeLink/linkDetector can also
emit a 2-token form like [link, "."] (e.g., "example.com."). Update
isValidLinkStructure to also return true when tokens.length === 2 and the first
token is a link and the second token is a trailing punctuation dot
(tokens[0].isLink && tokens[1].value === ".") while keeping the existing
single-token and 3-token ("()", "[]") logic so the validator stays in sync with
tokenizeLink()/linkDetector behavior.

Comment on lines +822 to +855
it("does not apply link when pasting URL with empty selection", () => {
editor = createEditor();
editor.replaceBlocks(editor.document, [
{
id: "test-block",
type: "paragraph",
content: "some text",
},
]);

// Place cursor without selection
editor.setTextCursorPosition("test-block", "end");
const view = editor._tiptapEditor.view;

const pastePlugin = view.state.plugins.find(
(p) => (p as any).key === "handlePasteLink$"
);

if (pastePlugin && pastePlugin.props.handlePaste) {
const { Slice, Fragment } = require("@tiptap/pm/model");
const textNode = view.state.schema.text("https://example.com");
const slice = new Slice(Fragment.from(textNode), 0, 0);

const result = (pastePlugin.props.handlePaste as any)(
view,
new ClipboardEvent("paste"),
slice
);

// Should return false because selection is empty
expect(result).toBe(false);
}
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same conditional expect pattern - refactor for reliability.

Apply the same fix here to ensure the test actually runs its assertions.

Proposed fix
   it("does not apply link when pasting URL with empty selection", () => {
     // ... setup code ...
 
     const pastePlugin = view.state.plugins.find(
       (p) => (p as any).key === "handlePasteLink$"
     );
+    expect(pastePlugin).toBeDefined();
+    expect(pastePlugin!.props.handlePaste).toBeDefined();
 
-    if (pastePlugin && pastePlugin.props.handlePaste) {
-      const { Slice, Fragment } = require("@tiptap/pm/model");
-      const textNode = view.state.schema.text("https://example.com");
-      const slice = new Slice(Fragment.from(textNode), 0, 0);
+    const textNode = view.state.schema.text("https://example.com");
+    const slice = new Slice(Fragment.from(textNode), 0, 0);
 
-      const result = (pastePlugin.props.handlePaste as any)(
-        view,
-        new ClipboardEvent("paste"),
-        slice
-      );
+    const result = (pastePlugin!.props.handlePaste as any)(
+      view,
+      new ClipboardEvent("paste"),
+      slice
+    );
 
-      // Should return false because selection is empty
-      expect(result).toBe(false);
-    }
+    // Should return false because selection is empty
+    expect(result).toBe(false);
   });
🧰 Tools
🪛 ESLint

[error] 841-841: Require statement not part of import statement.

(@typescript-eslint/no-var-requires)


[error] 852-852: Avoid calling expect conditionally`

(jest/no-conditional-expect)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/tiptap-extensions/Link/link.test.ts` around
lines 822 - 855, The test currently guards the paste plugin call with an if so
assertions may be skipped; instead assert the plugin and its handler exist
before invoking: locate the test "does not apply link when pasting URL with
empty selection" and replace the conditional block that finds pastePlugin with
explicit expectations (e.g., expect(pastePlugin).toBeDefined() and
expect(pastePlugin.props.handlePaste).toBeDefined()), then call
pastePlugin.props.handlePaste(...) and assert the returned value is false; keep
references to createEditor, editor.replaceBlocks, editor.setTextCursorPosition,
and pastePlugin.props.handlePaste to find and update the code.

…e conversion logic

- Remove unused setLink/toggleLink/unsetLink TipTap commands from Link mark extension
- Move editLink/deleteLink/getLinkMarkAtPos from LinkToolbar into StyleManager,
  exposing them as public API on BlockNoteEditor
- LinkToolbar now delegates to editor API instead of doing raw mark operations
- Rewrite contentNodeToInlineContent as a two-pass flatten-then-merge approach,
  replacing ~200 lines of nested state machine
- Simplify linkToNodes in blockToNode.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/core/src/extensions/LinkToolbar/LinkToolbar.ts (1)

53-55: Consider documenting or deprecating the unused _markType parameter.

The _markType parameter is no longer used since the function always delegates to getLinkAtPos. While removing it might break external consumers, a deprecation notice or JSDoc clarification would help maintain clarity.

Suggested documentation
-    getMarkAtPos(pos: number, _markType: string) {
+    /**
+     * `@deprecated` markType parameter is ignored; always returns link mark
+     */
+    getMarkAtPos(pos: number, _markType: string) {
       return getLinkAtPos(pos);
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/LinkToolbar/LinkToolbar.ts` around lines 53 -
55, The getMarkAtPos method currently accepts an unused parameter _markType and
simply calls getLinkAtPos; update the code by adding a JSDoc deprecation note
for the _markType parameter (or for the whole method if appropriate) explaining
it is unused and will be removed in a future release, and document that callers
should rely on getLinkAtPos instead; ensure the JSDoc is placed above
getMarkAtPos and references the parameter name _markType and the delegating
function getLinkAtPos so consumers and linters can see the deprecation and
migration path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/core/src/extensions/LinkToolbar/LinkToolbar.ts`:
- Around line 53-55: The getMarkAtPos method currently accepts an unused
parameter _markType and simply calls getLinkAtPos; update the code by adding a
JSDoc deprecation note for the _markType parameter (or for the whole method if
appropriate) explaining it is unused and will be removed in a future release,
and document that callers should rely on getLinkAtPos instead; ensure the JSDoc
is placed above getMarkAtPos and references the parameter name _markType and the
delegating function getLinkAtPos so consumers and linters can see the
deprecation and migration path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ac9dddc-660b-4fbc-992a-9deca937d7ac

📥 Commits

Reviewing files that changed from the base of the PR and between 989fb85 and 688d767.

⛔ Files ignored due to path filters (7)
  • packages/xl-ai/src/prosemirror/__snapshots__/agent.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
  • packages/xl-ai/src/prosemirror/__snapshots__/changeset.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/between-links.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/link.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/adjacent.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/basic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/styled.json is excluded by !**/__snapshots__/**
📒 Files selected for processing (6)
  • packages/core/src/api/nodeConversions/blockToNode.ts
  • packages/core/src/api/nodeConversions/nodeToBlock.ts
  • packages/core/src/editor/BlockNoteEditor.ts
  • packages/core/src/editor/managers/StyleManager.ts
  • packages/core/src/extensions/LinkToolbar/LinkToolbar.ts
  • packages/core/src/extensions/tiptap-extensions/Link/link.ts

- Fix no-useless-escape warnings in linkDetector.ts regex patterns
- Fix curly brace requirements in linkDetector.ts and link.ts
- Fix prefer-const in linkDetector.ts
- Replace require() with ES imports in link.test.ts
- Fix jest/no-conditional-expect by using view.someProp for paste tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/extensions/tiptap-extensions/Link/link.test.ts (1)

712-801: Consider extracting shared paste-test setup helpers.

applies link and does not apply link duplicate block setup + selection discovery. A small helper (setupEditorWithSelectedText, createPasteSlice) would reduce maintenance noise as cases grow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/tiptap-extensions/Link/link.test.ts` around
lines 712 - 801, Extract the duplicated setup into two small helpers in
link.test.ts: create setupEditorWithSelectedText that initializes
createEditor(), calls replaceBlocks with the test paragraph, sets the text
cursor, finds and returns {editor, view, textStart, textEnd} (use the existing
selection discovery logic), and create createPasteSlice that takes a string and
returns a Slice built from view.state.schema.text(...); then refactor both tests
to call setupEditorWithSelectedText() and createPasteSlice("...") and use the
returned editor/view/positions to set the selection and dispatch the paste; keep
test assertions identical and reuse getLinksInDocument as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/extensions/tiptap-extensions/Link/link.test.ts`:
- Around line 728-735: The test computes textStart/textEnd by scanning
doc.descendants but leaves them at 0 when the "click here" node isn't found,
causing a false-positive path; change the initialization to sentinel values
(e.g., textStart = -1, textEnd = -1) or add an explicit assertion immediately
after the descendants loop that the node was found (e.g.,
expect(textStart).not.toBe(-1) / expect(textEnd).toBeGreaterThan(textStart)),
and apply this assertion in both paste-related tests that compute
textStart/textEnd so the selected-text branch is actually exercised.

---

Nitpick comments:
In `@packages/core/src/extensions/tiptap-extensions/Link/link.test.ts`:
- Around line 712-801: Extract the duplicated setup into two small helpers in
link.test.ts: create setupEditorWithSelectedText that initializes
createEditor(), calls replaceBlocks with the test paragraph, sets the text
cursor, finds and returns {editor, view, textStart, textEnd} (use the existing
selection discovery logic), and create createPasteSlice that takes a string and
returns a Slice built from view.state.schema.text(...); then refactor both tests
to call setupEditorWithSelectedText() and createPasteSlice("...") and use the
returned editor/view/positions to set the selection and dispatch the paste; keep
test assertions identical and reuse getLinksInDocument as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d833076-f2cf-489c-8856-b93e87939b6f

📥 Commits

Reviewing files that changed from the base of the PR and between 688d767 and 9079aa8.

📒 Files selected for processing (3)
  • packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts
  • packages/core/src/extensions/tiptap-extensions/Link/link.test.ts
  • packages/core/src/extensions/tiptap-extensions/Link/link.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/src/extensions/tiptap-extensions/Link/link.ts
  • packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts

Comment on lines +728 to +735
let textStart = 0;
let textEnd = 0;
doc.descendants((node, pos) => {
if (node.isText && node.text === "click here") {
textStart = pos;
textEnd = pos + node.nodeSize;
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert selection bounds before invoking paste behavior.

If the "click here" text node isn’t found, the test falls back to 0..0 selection and the non-URL case can pass without exercising the “selected text” path.

Proposed reliability fix
     let textStart = 0;
     let textEnd = 0;
     doc.descendants((node, pos) => {
       if (node.isText && node.text === "click here") {
         textStart = pos;
         textEnd = pos + node.nodeSize;
       }
     });
+    expect(textEnd).toBeGreaterThan(textStart);

     const tr = view.state.tr.setSelection(
       TextSelection.create(view.state.doc, textStart, textEnd)
     );

Apply the same assertion in both paste tests that compute textStart/textEnd.

Also applies to: 774-781

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/tiptap-extensions/Link/link.test.ts` around
lines 728 - 735, The test computes textStart/textEnd by scanning doc.descendants
but leaves them at 0 when the "click here" node isn't found, causing a
false-positive path; change the initialization to sentinel values (e.g.,
textStart = -1, textEnd = -1) or add an explicit assertion immediately after the
descendants loop that the node was found (e.g., expect(textStart).not.toBe(-1) /
expect(textEnd).toBeGreaterThan(textStart)), and apply this assertion in both
paste-related tests that compute textStart/textEnd so the selected-text branch
is actually exercised.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 3, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2623

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2623

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2623

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2623

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2623

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2623

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2623

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2623

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2623

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2623

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2623

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2623

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2623

commit: 28b2a44

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 3, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2623

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2623

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2623

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2623

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2623

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2623

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2623

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2623

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2623

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2623

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2623

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2623

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2623

commit: 9079aa8

- Fix double mailto: prefix for mailto: URLs in linkToken() — values
  starting with "mailto:" are now classified as "url" not "email"
- Support schemeless URLs with ? or # suffixes (e.g. example.com?x=1,
  example.com#frag) in both SCHEMELESS_RE and isSingleUrl()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts`:
- Around line 393-396: The email detection is case-sensitive and can
mis-classify inputs like "MAILTO:..." causing a double "mailto:" prefix; in
linkDetector.ts update the mailto check to be case-insensitive by using
value.toLowerCase().startsWith("mailto:") (i.e., change the
!value.startsWith("mailto:") test to !value.toLowerCase().startsWith("mailto:"))
while keeping the rest of the expression (value.includes("@") and
!value.includes("://")) untouched so buildHref will no longer prepend a second
"mailto:".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c04a3e41-bb2f-46d0-acba-1fd01c4a2be4

📥 Commits

Reviewing files that changed from the base of the PR and between 9079aa8 and 1e7e92c.

📒 Files selected for processing (1)
  • packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts

Comment on lines +393 to +396
const type =
value.includes("@") && !value.includes("://") && !value.startsWith("mailto:")
? "email"
: "url";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Case-sensitive mailto: check can still cause double prefix.

The fix for the double mailto: prefix issue is case-sensitive. Input like MAILTO:user@example.com would be classified as "email" (since "MAILTO:".startsWith("mailto:") is false), causing buildHref to return "mailto:MAILTO:user@example.com".

Proposed fix
 function linkToken(
   value: string,
   start: number,
   end: number,
   defaultProtocol: string
 ): LinkMatch {
   const type =
-    value.includes("@") && !value.includes("://") && !value.startsWith("mailto:")
+    value.includes("@") && !value.includes("://") && !/^mailto:/i.test(value)
       ? "email"
       : "url";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts`
around lines 393 - 396, The email detection is case-sensitive and can
mis-classify inputs like "MAILTO:..." causing a double "mailto:" prefix; in
linkDetector.ts update the mailto check to be case-insensitive by using
value.toLowerCase().startsWith("mailto:") (i.e., change the
!value.startsWith("mailto:") test to !value.toLowerCase().startsWith("mailto:"))
while keeping the rest of the expression (value.includes("@") and
!value.includes("://")) untouched so buildHref will no longer prepend a second
"mailto:".

The ^1.4.15 specifier allowed better-auth 1.5.6 to be installed,
which removed the createAuthMiddleware export and broke the docs build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant