diff --git a/package-lock.json b/package-lock.json index deb52d2..c3927fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "d3": "^7.9.0", "he": "1.2.x", "highlight.js": "^11.11.1", + "node-emoji": "^2.2.0", "nostr-tools": "2.10.x" }, "devDependencies": { @@ -24,7 +25,7 @@ "@sveltejs/adapter-auto": "3.x", "@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-static": "3.x", - "@sveltejs/kit": "2.x", + "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "4.x", "@types/d3": "^7.4.3", "@types/he": "1.2.x", @@ -1364,6 +1365,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sveltejs/adapter-auto": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", @@ -2454,6 +2467,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", @@ -3162,6 +3184,12 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4775,6 +4803,21 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "license": "ISC" }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -5806,6 +5849,18 @@ "node": ">=18" } }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6542,6 +6597,15 @@ "dev": true, "license": "MIT" }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unxhr": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.2.0.tgz", diff --git a/package.json b/package.json index 3774cd4..6993ab2 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "d3": "^7.9.0", "he": "1.2.x", "highlight.js": "^11.11.1", + "node-emoji": "^2.2.0", "nostr-tools": "2.10.x" }, "devDependencies": { diff --git a/src/lib/utils/markdown/basicMarkdownParser.ts b/src/lib/utils/markdown/basicMarkdownParser.ts index 5df1252..c06e840 100644 --- a/src/lib/utils/markdown/basicMarkdownParser.ts +++ b/src/lib/utils/markdown/basicMarkdownParser.ts @@ -1,4 +1,5 @@ import { processNostrIdentifiers } from '../nostrUtils'; +import * as emoji from 'node-emoji'; // Regular expressions for basic markdown elements const BOLD_REGEX = /(\*\*|[*])((?:[^*\n]|\*(?!\*))+)\1/g; @@ -25,6 +26,8 @@ const VIDEO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp4|webm|mov|avi)(?:[^\s<]*)?/i; const AUDIO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp3|wav|ogg|m4a)(?:[^\s<]*)?/i; const YOUTUBE_URL_REGEX = /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/i; +// Emoji shortcut pattern +const EMOJI_SHORTCUT_REGEX = /:([a-zA-Z0-9_+-]+):/g; function processBasicFormatting(content: string): string { if (!content) return ''; @@ -133,6 +136,18 @@ function processBlockquotes(content: string): string { } } +function processEmojiShortcuts(content: string): string { + try { + return emoji.emojify(content, { fallback: (name: string) => { + const emojiChar = emoji.get(name); + return emojiChar || `:${name}:`; + }}); + } catch (error) { + console.error('Error in processEmojiShortcuts:', error); + return content; + } +} + export async function parseBasicMarkdown(text: string): Promise { if (!text) return ''; @@ -140,6 +155,9 @@ export async function parseBasicMarkdown(text: string): Promise { // Process basic text formatting first let processedText = processBasicFormatting(text); + // Process emoji shortcuts + processedText = processEmojiShortcuts(processedText); + // Process lists - handle ordered lists first processedText = processedText // Process ordered lists diff --git a/src/lib/utils/markdown/markdownTestfile.md b/src/lib/utils/markdown/markdownTestfile.md index cb35194..7ceb3b8 100644 --- a/src/lib/utils/markdown/markdownTestfile.md +++ b/src/lib/utils/markdown/markdownTestfile.md @@ -70,6 +70,16 @@ Here with a naddr: nostr:naddr1qvzqqqr4gupzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqydhwumn8ghj7argv4nx7un9wd6zumn0wd68yvfwvdhk6tcpr3mhxue69uhkx6rjd9ehgurfd3kzumn0wd68yvfwvdhk6tcqzasj6ar9wd6xv6tvv5kkvmmj94kkzuntv3hhwmsu0ktnz +Here's a nonsense one: + +nevent123 + +And some Nostr addresses that should be ignored: + +https://lumina.rocks/note/note1sd0hkhxr49jsetkcrjkvf2uls5m8frkue6f5huj8uv4964p2d8fs8dn68z + +https://primal.net/e/nevent1qqsqum7j25p9z8vcyn93dsd7edx34w07eqav50qnde3vrfs466q558gdd02yr + This is an implementation of [Nostr-flavored Markdown](https://github.com/nostrability/nostrability/issues/146) for #gitstuff issue notes. You can even include `code inline`, like `
` or @@ -162,6 +172,8 @@ content starts at 4-columns in. > if you like. ``` +Test out some emojis :heart: and :trophy: + #### Here is an image! ![Nostr logo](https://user-images.githubusercontent.com/99301796/219900773-d6d02038-e2a0-4334-9f28-c14d40ab6fe7.png) diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 39c3ec4..e63f9b4 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -118,13 +118,27 @@ function createNoteLink(identifier: string): string { * Process Nostr identifiers in text */ export async function processNostrIdentifiers(content: string): Promise { - console.log('Processing Nostr identifiers:', { input: content }); let processedContent = content; + // Helper to check if a match is part of a URL + function isPartOfUrl(text: string, index: number): boolean { + // Look for http(s):// or www. before the match + const before = text.slice(Math.max(0, index - 12), index); + return /https?:\/\/$|www\.$/i.test(before); + } + // Process profiles (npub and nprofile) const profileMatches = Array.from(content.matchAll(NOSTR_PROFILE_REGEX)); for (const match of profileMatches) { - const [fullMatch, identifier] = match; + const [fullMatch] = match; + const matchIndex = match.index ?? 0; + if (isPartOfUrl(content, matchIndex)) { + continue; // skip if part of a URL + } + let identifier = fullMatch; + if (!identifier.startsWith('nostr:')) { + identifier = 'nostr:' + identifier; + } const metadata = await getUserMetadata(identifier); const displayText = metadata.displayName || metadata.name; const link = createProfileLink(identifier, displayText); @@ -134,7 +148,15 @@ export async function processNostrIdentifiers(content: string): Promise // Process notes (nevent, note, naddr) const noteMatches = Array.from(processedContent.matchAll(NOSTR_NOTE_REGEX)); for (const match of noteMatches) { - const [fullMatch, identifier] = match; + const [fullMatch] = match; + const matchIndex = match.index ?? 0; + if (isPartOfUrl(processedContent, matchIndex)) { + continue; // skip if part of a URL + } + let identifier = fullMatch; + if (!identifier.startsWith('nostr:')) { + identifier = 'nostr:' + identifier; + } const link = createNoteLink(identifier); processedContent = processedContent.replace(fullMatch, link); }