diff --git a/src/features/messages/markdown-preparation.ts b/src/features/messages/markdown-preparation.ts index 42f185c33f..cccd40ea63 100644 --- a/src/features/messages/markdown-preparation.ts +++ b/src/features/messages/markdown-preparation.ts @@ -36,7 +36,7 @@ export const isLiteralMarkdownContext = (type: string) => "html", ].includes(type); -function literalRanges( +export function literalRanges( tree: MarkdownNode, includeLinks: boolean, ): readonly LiteralRange[] { diff --git a/src/features/messages/message-edit.test.ts b/src/features/messages/message-edit.test.ts index 422a57bb82..de6e977770 100644 --- a/src/features/messages/message-edit.test.ts +++ b/src/features/messages/message-edit.test.ts @@ -75,6 +75,25 @@ describe("message edit round trips", () => { const row = firstRow([{ ...original, tags: [["h", "room"]] }]); expect(messageEditText(row, profiles)).toBe(original.content); }); + it.each([ + "[ref]: ", + '[ref]: https://example.com "Ask @Sam"', + "", + 'Hi', + '[[a]](https://example.com "See @Sam")', + '[a](https://example.com\n "See @Sam")', + "[hello\n@Sam](https://example.com)", + "> @Sam", + ])("links a name beside literal Markdown, not inside it: %s", (literal) => { + const content = `@Sam\n\n${literal}`; + const tags = [ + ["h", "room"], + ["p", person], + ]; + expect( + messageEditText(firstRow([{ ...original, content, tags }]), profiles), + ).toBe(`[@Sam](${profileTarget(person)})\n\n${literal}`); + }); }); it("rejects changed attachment links while allowing text-only edits", () => { diff --git a/src/features/messages/message-edit.ts b/src/features/messages/message-edit.ts index d7b9bf891f..7a21bb3d2e 100644 --- a/src/features/messages/message-edit.ts +++ b/src/features/messages/message-edit.ts @@ -1,7 +1,11 @@ -import { projectMarkdownAttachments } from "../relay/message-content"; +import { + projectMarkdownAttachments, + scanMarkdown, +} from "../relay/message-content"; import type { AgentLibrary } from "../agents/library"; import type { ChannelMessage, Profile } from "../relay/contracts"; import { npubEncode } from "nostr-tools/nip19"; +import { literalRanges } from "./markdown-preparation"; import type { MentionRecipient } from "./mention-draft"; import { profileMentionParts } from "./profile-mentions"; @@ -27,14 +31,19 @@ export function messageEditText( agents: AgentLibrary["identities"] = [], ) { const { attachmentSeams: _seams, ...source } = row; - return profileMentionParts( - { ...source, content: row.sourceContent ?? row.content }, - profiles, - agents, - ) - .map(({ text, target }) => - target ? profileMentionLink(text, target) : text, - ) + const content = row.sourceContent ?? row.content; + const ranges = literalRanges(scanMarkdown(content).tree, true); + let offset = 0; + let next = 0; + return profileMentionParts({ ...source, content }, profiles, agents) + .map(({ text, target }) => { + const start = offset; + offset += text.length; + while ((ranges[next]?.end ?? Infinity) <= start) next++; + return target && (ranges[next]?.start ?? Infinity) >= offset + ? profileMentionLink(text, target) + : text; + }) .join(""); }