Fix nostr entity matching in paste handler (#197)

* fix: only match nostr entities at word boundaries in paste handler

Updates the paste handler regex to only match nostr bech32 entities
(npub, note, nevent, naddr, nprofile) when surrounded by whitespace or
at string boundaries. This prevents URLs containing nostr entities
(e.g., https://njump.me/npub1...) from being incorrectly converted
to mentions.

Uses a capture group (^|\s) instead of lookbehind assertion for
Safari compatibility (lookbehind only supported in Safari 16.4+).

* fix: disable pointer events on links in editor

Adds pointer-events: none to anchor tags within the ProseMirror editor
to prevent clicking on pasted URLs from navigating away. This allows
users to edit the text containing links rather than accidentally
triggering navigation.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-22 13:13:40 +01:00
committed by GitHub
parent f329e9c766
commit b83b26ea9a
2 changed files with 20 additions and 9 deletions

View File

@@ -39,8 +39,11 @@ export const NostrPasteHandler = Extension.create({
if (!text) return false; if (!text) return false;
// Regex to detect nostr bech32 strings (with or without nostr: prefix) // Regex to detect nostr bech32 strings (with or without nostr: prefix)
// Only match entities surrounded by whitespace or at string boundaries
// to avoid matching entities within URLs (e.g., https://njump.me/npub1...)
// Note: Using (^|\s) capture group instead of lookbehind for Safari compatibility
const bech32Regex = const bech32Regex =
/(?:nostr:)?(npub1[\w]{58,}|note1[\w]{58,}|nevent1[\w]+|naddr1[\w]+|nprofile1[\w]+)/g; /(^|\s)(?:nostr:)?(npub1[\w]{58,}|note1[\w]{58,}|nevent1[\w]+|naddr1[\w]+|nprofile1[\w]+)(?=$|\s)/g;
const matches = Array.from(text.matchAll(bech32Regex)); const matches = Array.from(text.matchAll(bech32Regex));
if (matches.length === 0) return false; // No bech32 found, use default paste if (matches.length === 0) return false; // No bech32 found, use default paste
@@ -50,13 +53,15 @@ export const NostrPasteHandler = Extension.create({
let lastIndex = 0; let lastIndex = 0;
for (const match of matches) { for (const match of matches) {
const matchedText = match[0]; const fullMatch = match[0];
const boundary = match[1]; // Leading whitespace or empty (start of string)
const bech32 = match[2]; // The bech32 without nostr: prefix
const matchIndex = match.index!; const matchIndex = match.index!;
const bech32 = match[1]; // The bech32 without nostr: prefix
// Add text before this match // Add text before this match (including the boundary whitespace)
if (lastIndex < matchIndex) { const textBeforeEnd = matchIndex + boundary.length;
const textBefore = text.slice(lastIndex, matchIndex); if (lastIndex < textBeforeEnd) {
const textBefore = text.slice(lastIndex, textBeforeEnd);
if (textBefore) { if (textBefore) {
nodes.push(view.state.schema.text(textBefore)); nodes.push(view.state.schema.text(textBefore));
} }
@@ -111,16 +116,17 @@ export const NostrPasteHandler = Extension.create({
// Add space after preview node // Add space after preview node
nodes.push(view.state.schema.text(" ")); nodes.push(view.state.schema.text(" "));
} catch (err) { } catch (err) {
// Invalid bech32, insert as plain text // Invalid bech32, insert as plain text (entity portion without boundary)
console.warn( console.warn(
"[NostrPasteHandler] Failed to decode:", "[NostrPasteHandler] Failed to decode:",
bech32, bech32,
err, err,
); );
nodes.push(view.state.schema.text(matchedText)); const entityText = fullMatch.slice(boundary.length);
nodes.push(view.state.schema.text(entityText));
} }
lastIndex = matchIndex + matchedText.length; lastIndex = matchIndex + fullMatch.length;
} }
// Add remaining text after last match // Add remaining text after last match

View File

@@ -392,6 +392,11 @@ body.animating-layout
top: 0; top: 0;
} }
/* Disable link navigation in editor - allow editing instead of clicking */
.ProseMirror a {
pointer-events: none;
}
/* Mention styles */ /* Mention styles */
.ProseMirror .mention { .ProseMirror .mention {
color: hsl(var(--primary)); color: hsl(var(--primary));