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,
};