From d6a5a82cf881c5ea06323e9eb171c78a069eb2a1 Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 24 Aug 2025 15:54:13 +0800 Subject: [PATCH] refactor: url parser --- src/components/Content/index.tsx | 10 +-- src/components/ContentPreview/Content.tsx | 10 +-- src/components/ProfileAbout/index.tsx | 12 ++-- src/lib/content-parser.ts | 74 ++++++++++++++++------- src/lib/event-metadata.ts | 2 +- src/lib/url.ts | 18 +++++- 6 files changed, 77 insertions(+), 49 deletions(-) diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx index baeb4a2b..e1454d46 100644 --- a/src/components/Content/index.tsx +++ b/src/components/Content/index.tsx @@ -3,13 +3,10 @@ import { EmbeddedEmojiParser, EmbeddedEventParser, EmbeddedHashtagParser, - EmbeddedImageParser, EmbeddedLNInvoiceParser, - EmbeddedMediaParser, EmbeddedMentionParser, - EmbeddedNormalUrlParser, + EmbeddedUrlParser, EmbeddedWebsocketUrlParser, - EmbeddedYoutubeParser, parseContent } from '@/lib/content-parser' import { getImageInfosFromEvent } from '@/lib/event' @@ -40,10 +37,7 @@ const Content = memo( if (!_content) return null const nodes = parseContent(_content, [ - EmbeddedYoutubeParser, - EmbeddedImageParser, - EmbeddedMediaParser, - EmbeddedNormalUrlParser, + EmbeddedUrlParser, EmbeddedLNInvoiceParser, EmbeddedWebsocketUrlParser, EmbeddedEventParser, diff --git a/src/components/ContentPreview/Content.tsx b/src/components/ContentPreview/Content.tsx index 371d0469..82531903 100644 --- a/src/components/ContentPreview/Content.tsx +++ b/src/components/ContentPreview/Content.tsx @@ -1,9 +1,8 @@ import { EmbeddedEmojiParser, EmbeddedEventParser, - EmbeddedImageParser, EmbeddedMentionParser, - EmbeddedMediaParser, + EmbeddedUrlParser, parseContent } from '@/lib/content-parser' import { cn } from '@/lib/utils' @@ -25,8 +24,7 @@ export default function Content({ const { t } = useTranslation() const nodes = useMemo(() => { return parseContent(content, [ - EmbeddedImageParser, - EmbeddedMediaParser, + EmbeddedUrlParser, EmbeddedEventParser, EmbeddedMentionParser, EmbeddedEmojiParser @@ -36,9 +34,6 @@ export default function Content({ return ( {nodes.map((node, index) => { - if (node.type === 'text') { - return node.data - } if (node.type === 'image' || node.type === 'images') { return index > 0 ? ` [${t('image')}]` : `[${t('image')}]` } @@ -57,6 +52,7 @@ export default function Content({ if (!emoji) return node.data return } + return node.data })} ) diff --git a/src/components/ProfileAbout/index.tsx b/src/components/ProfileAbout/index.tsx index 1cf9e9ba..8d14cf87 100644 --- a/src/components/ProfileAbout/index.tsx +++ b/src/components/ProfileAbout/index.tsx @@ -1,21 +1,21 @@ import { EmbeddedHashtagParser, EmbeddedMentionParser, - EmbeddedNormalUrlParser, + EmbeddedUrlParser, EmbeddedWebsocketUrlParser, parseContent } from '@/lib/content-parser' import { detectLanguage } from '@/lib/utils' +import { useTranslationService } from '@/providers/TranslationServiceProvider' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import { EmbeddedHashtag, EmbeddedMention, EmbeddedNormalUrl, EmbeddedWebsocketUrl } from '../Embedded' -import { useTranslationService } from '@/providers/TranslationServiceProvider' -import { toast } from 'sonner' export default function ProfileAbout({ about, className }: { about?: string; className?: string }) { const { t, i18n } = useTranslation() @@ -33,14 +33,11 @@ export default function ProfileAbout({ about, className }: { about?: string; cla const nodes = parseContent(translatedAbout ?? about, [ EmbeddedWebsocketUrlParser, - EmbeddedNormalUrlParser, + EmbeddedUrlParser, EmbeddedHashtagParser, EmbeddedMentionParser ]) return nodes.map((node, index) => { - if (node.type === 'text') { - return node.data - } if (node.type === 'url') { return } @@ -53,6 +50,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla if (node.type === 'mention') { return } + return node.data }) }, [about, translatedAbout]) diff --git a/src/lib/content-parser.ts b/src/lib/content-parser.ts index 10349c38..24c758aa 100644 --- a/src/lib/content-parser.ts +++ b/src/lib/content-parser.ts @@ -3,13 +3,12 @@ import { EMBEDDED_MENTION_REGEX, EMOJI_SHORT_CODE_REGEX, HASHTAG_REGEX, - IMAGE_REGEX, LN_INVOICE_REGEX, URL_REGEX, - MEDIA_REGEX, WS_URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants' +import { isImage, isMedia } from './url' export type TEmbeddedNodeType = | 'text' @@ -36,7 +35,9 @@ export type TEmbeddedNode = data: string[] } -type TContentParser = { type: Exclude; regex: RegExp } +type TContentParser = + | { type: Exclude; regex: RegExp } + | ((content: string) => TEmbeddedNode[]) export const EmbeddedHashtagParser: TContentParser = { type: 'hashtag', @@ -58,31 +59,11 @@ export const EmbeddedEventParser: TContentParser = { regex: EMBEDDED_EVENT_REGEX } -export const EmbeddedImageParser: TContentParser = { - type: 'image', - regex: IMAGE_REGEX -} - -export const EmbeddedMediaParser: TContentParser = { - type: 'media', - regex: MEDIA_REGEX -} - export const EmbeddedWebsocketUrlParser: TContentParser = { type: 'websocket-url', regex: WS_URL_REGEX } -export const EmbeddedNormalUrlParser: TContentParser = { - type: 'url', - regex: URL_REGEX -} - -export const EmbeddedYoutubeParser: TContentParser = { - type: 'youtube', - regex: YOUTUBE_URL_REGEX -} - export const EmbeddedEmojiParser: TContentParser = { type: 'emoji', regex: EMOJI_SHORT_CODE_REGEX @@ -93,6 +74,48 @@ export const EmbeddedLNInvoiceParser: TContentParser = { regex: LN_INVOICE_REGEX } +export const EmbeddedUrlParser: TContentParser = (content: string) => { + const matches = content.matchAll(URL_REGEX) + const result: TEmbeddedNode[] = [] + let lastIndex = 0 + for (const match of matches) { + const matchStart = match.index! + // Add text before the match + if (matchStart > lastIndex) { + result.push({ + type: 'text', + data: content.slice(lastIndex, matchStart) + }) + } + + const url = match[0] + let type: TEmbeddedNodeType = 'url' + if (isImage(url)) { + type = 'image' + } else if (isMedia(url)) { + type = 'media' + } else if (YOUTUBE_URL_REGEX.test(url)) { + type = 'youtube' + } + + // Add the match as specific type + result.push({ + type, + data: url + }) + + lastIndex = matchStart + url.length + } + // Add text after the last match + if (lastIndex < content.length) { + result.push({ + type: 'text', + data: content.slice(lastIndex) + }) + } + return result +} + export function parseContent(content: string, parsers: TContentParser[]) { let nodes: TEmbeddedNode[] = [{ type: 'text', data: content.trim() }] @@ -100,6 +123,11 @@ export function parseContent(content: string, parsers: TContentParser[]) { nodes = nodes .flatMap((node) => { if (node.type !== 'text') return [node] + + if (typeof parser === 'function') { + return parser(node.data) + } + const matches = node.data.matchAll(parser.regex) const result: TEmbeddedNode[] = [] let lastIndex = 0 diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts index 1a3e941e..5dc09399 100644 --- a/src/lib/event-metadata.ts +++ b/src/lib/event-metadata.ts @@ -70,7 +70,7 @@ export function getProfileFromEvent(event: Event) { created_at: event.created_at } } catch (err) { - console.error(err) + console.error(event.content, err) return { pubkey: event.pubkey, npub: pubkeyToNpub(event.pubkey) ?? '', diff --git a/src/lib/url.ts b/src/lib/url.ts index 054c4cb5..bc723096 100644 --- a/src/lib/url.ts +++ b/src/lib/url.ts @@ -117,10 +117,22 @@ export function isImage(url: string) { } } -export function isVideo(url: string) { +export function isMedia(url: string) { try { - const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov'] - return videoExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext)) + const mediaExtensions = [ + '.mp4', + '.webm', + '.ogg', + '.mov', + '.mp3', + '.wav', + '.flac', + '.aac', + '.m4a', + '.opus', + '.wma' + ] + return mediaExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext)) } catch { return false }