diff --git a/src/utils/note.test.ts b/src/utils/note.test.ts index 0c9c6bf8..67f802c6 100644 --- a/src/utils/note.test.ts +++ b/src/utils/note.test.ts @@ -10,6 +10,27 @@ Deno.test('parseNoteContent', () => { assertEquals(firstUrl, undefined); }); +Deno.test('parseNoteContent handles apostrophes', () => { + const { html } = parseNoteContent( + `did you see nostr:nprofile1qqsqgc0uhmxycvm5gwvn944c7yfxnnxm0nyh8tt62zhrvtd3xkj8fhgprdmhxue69uhkwmr9v9ek7mnpw3hhytnyv4mz7un9d3shjqgcwaehxw309ahx7umywf5hvefwv9c8qtmjv4kxz7gpzemhxue69uhhyetvv9ujumt0wd68ytnsw43z7s3al0v's speech?`, + [{ + id: '0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd', + username: 'alex', + acct: 'alex@gleasonator.dev', + url: 'https://gleasonator.dev/@alex', + }], + ); + assertEquals( + html, + `did you see @alex@gleasonator.dev's speech?`, + ); +}); + +Deno.test("parseNoteContent doesn't parse invalid nostr URIs", () => { + const { html } = parseNoteContent(`nip19 has URIs like nostr:npub and nostr:nevent, etc.`, []); + assertEquals(html, 'nip19 has URIs like nostr:npub and nostr:nevent, etc.'); +}); + Deno.test('getMediaLinks', () => { const links = [ { href: 'https://example.com/image.png' }, diff --git a/src/utils/note.ts b/src/utils/note.ts index aba3d041..da5197d5 100644 --- a/src/utils/note.ts +++ b/src/utils/note.ts @@ -1,10 +1,11 @@ import 'linkify-plugin-hashtag'; import linkifyStr from 'linkify-string'; import linkify from 'linkifyjs'; -import { nip19, nip21, nip27 } from 'nostr-tools'; +import { nip19, nip27 } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { MastodonMention } from '@/entities/MastodonMention.ts'; +import { html } from '@/utils/html.ts'; import { getUrlMediaType, isPermittedMediaType } from '@/utils/media.ts'; linkify.registerCustomProtocol('nostr', true); @@ -23,29 +24,33 @@ interface ParsedNoteContent { function parseNoteContent(content: string, mentions: MastodonMention[]): ParsedNoteContent { const links = linkify.find(content).filter(isLinkURL); const firstUrl = links.find(isNonMediaLink)?.href; - const html = linkifyStr(content, { + + const result = linkifyStr(content, { render: { hashtag: ({ content }) => { const tag = content.replace(/^#/, ''); const href = Conf.local(`/tags/${tag}`); - return `#${tag}`; + return html`#${tag}`; }, url: ({ attributes, content }) => { - const extra = content.slice(69) - content = content.slice(0,69) try { - const { decoded } = nip21.parse(content); - const pubkey = getDecodedPubkey(decoded); - if (pubkey) { - const mention = mentions.find((m) => m.id === pubkey); - const npub = nip19.npubEncode(pubkey); - const acct = mention?.acct ?? npub; - const name = mention?.acct ?? npub.substring(0, 8); - const href = mention?.url ?? Conf.local(`/@${acct}`); - return `@${name}${extra}`; - } else { - return ''; + const { pathname } = new URL(content); + const match = pathname.match(new RegExp(`^${nip19.BECH32_REGEX.source}`)); + if (match) { + const bech32 = match[0]; + const extra = pathname.slice(bech32.length); + const decoded = nip19.decode(bech32); + const pubkey = getDecodedPubkey(decoded); + if (pubkey) { + const mention = mentions.find((m) => m.id === pubkey); + const npub = nip19.npubEncode(pubkey); + const acct = mention?.acct ?? npub; + const name = mention?.acct ?? npub.substring(0, 8); + const href = mention?.url ?? Conf.local(`/@${acct}`); + return html`@${name}${extra}`; + } } + return content; } catch { const attr = Object.entries(attributes) .map(([name, value]) => `${name}="${value}"`) @@ -58,7 +63,7 @@ function parseNoteContent(content: string, mentions: MastodonMention[]): ParsedN }).replace(/\n+$/, ''); return { - html, + html: result, links, firstUrl, };