From ace58ecdd56b175a3f7ac138c6d10ce3ce011fb2 Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 28 Oct 2023 14:36:12 +0700 Subject: [PATCH] refactor text parser --- pnpm-lock.yaml | 26 ++--- src/shared/notes/kinds/text.tsx | 81 ++------------- src/shared/user.tsx | 4 +- src/utils/parser.ts | 124 ----------------------- src/utils/parser.tsx | 173 ++++++++++++++++++++++++++++++++ src/utils/types.d.ts | 3 +- 6 files changed, 200 insertions(+), 211 deletions(-) delete mode 100644 src/utils/parser.ts create mode 100644 src/utils/parser.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57e70a85..11781526 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,43 +51,43 @@ dependencies: specifier: ^5.0.5 version: 5.0.5(react-dom@18.2.0)(react@18.2.0) '@tauri-apps/api': - specifier: 2.0.0-alpha.9 + specifier: ^2.0.0-alpha.9 version: 2.0.0-alpha.9 '@tauri-apps/cli': - specifier: 2.0.0-alpha.16 + specifier: ^2.0.0-alpha.16 version: 2.0.0-alpha.16 '@tauri-apps/plugin-clipboard-manager': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-dialog': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-fs': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-http': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-notification': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-os': - specifier: 2.0.0-alpha.3 + specifier: ^2.0.0-alpha.3 version: 2.0.0-alpha.3 '@tauri-apps/plugin-process': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-shell': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-sql': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-updater': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tauri-apps/plugin-upload': - specifier: 2.0.0-alpha.2 + specifier: ^2.0.0-alpha.2 version: 2.0.0-alpha.2 '@tiptap/extension-character-count': specifier: ^2.1.12 diff --git a/src/shared/notes/kinds/text.tsx b/src/shared/notes/kinds/text.tsx index 2e14c2e1..bddf6f85 100644 --- a/src/shared/notes/kinds/text.tsx +++ b/src/shared/notes/kinds/text.tsx @@ -1,84 +1,23 @@ import { memo } from 'react'; -import ReactMarkdown from 'react-markdown'; -import { Link } from 'react-router-dom'; -import rehypeExternalLinks from 'rehype-external-links'; -import remarkGfm from 'remark-gfm'; -import { - Boost, - Hashtag, - ImagePreview, - Invoice, - LinkPreview, - MentionNote, - MentionUser, - VideoPreview, -} from '@shared/notes'; +import { ImagePreview, LinkPreview, MentionNote, VideoPreview } from '@shared/notes'; import { parser } from '@utils/parser'; export function TextNote(props: { content?: string }) { - const richContent = parser(props.content) ?? null; - - if (!richContent) { - return ( -
- - {props.content} - -
- ); - } + const richContent = parser(props.content); return (
- { - const cleanURL = new URL(href); - cleanURL.search = ''; - return ( - - {cleanURL.hostname + cleanURL.pathname} - - ); - }, - del: ({ children }) => { - const key = children[0] as string; - if (typeof key !== 'string') return; - if (key.startsWith('pub') && key.length > 50 && key.length < 100) { - return ; - } - if (key.startsWith('tag')) { - return ; - } - if (key.startsWith('boost')) { - return ; - } - if (key.startsWith('lnbc')) { - return ; - } - }, - }} - disallowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'code']} - unwrapDisallowed={true} - > +
{richContent.parsed} - - {richContent.images.length > 0 && } - {richContent.videos.length > 0 && } - {richContent.links.length > 0 && } - {richContent.notes.length > 0 && - richContent.notes.map((note: string) => )} +
+ {richContent.images.length ? : null} + {richContent.videos.length ? : null} + {richContent.links.length ? : null} + {richContent.notes.map((note: string) => ( + + ))}
); } diff --git a/src/shared/user.tsx b/src/shared/user.tsx index f9df835e..7371be2b 100644 --- a/src/shared/user.tsx +++ b/src/shared/user.tsx @@ -440,9 +440,9 @@ export const User = memo(function User({ if (status === 'pending') { return ( -
+
-
+
diff --git a/src/utils/parser.ts b/src/utils/parser.ts deleted file mode 100644 index ffafdd24..00000000 --- a/src/utils/parser.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { nip19 } from 'nostr-tools'; -import { AddressPointer, EventPointer, ProfilePointer } from 'nostr-tools/lib/nip19'; - -import { RichContent } from '@utils/types'; - -function isURL(string: string) { - try { - const url = new URL(string); - if (url.protocol.length > 0) { - if (url.protocol === 'https:' || url.protocol === 'http:') { - return true; - } else { - return false; - } - } - return true; - } catch (e) { - return false; - } -} - -export function parser(eventContent: string) { - if (!eventContent) return ''; - - try { - const content: RichContent = { - parsed: null, - images: [], - videos: [], - links: [], - notes: [], - }; - - const parse = eventContent.split(/\s/gm).map((word) => { - // url - if (isURL(word)) { - const url = new URL(word); - url.search = ''; - - if (url.pathname.match(/\.(jpg|jpeg|gif|png|webp|avif)$/)) { - // image url - content.images.push(word); - // remove url from original content - return word.replace(word, ''); - } - - if (url.pathname.match(/\.(mp4|mov|webm|wmv|flv|mts|avi|ogv|mkv|mp3|m3u8)$/)) { - // video - content.videos.push(word); - // remove url from original content - return word.replace(word, ''); - } - - content.links.push(url.toString()); - } - - // hashtag - if (word.startsWith('#') && word.length > 1) { - return word.replace(word, `~tag-${word}~`); - } - - // boost - if (word.startsWith('$prism') && word.length > 1) { - return word.replace(word, `~boost-${word}~`); - } - - // nostr account references (depreciated) - if (word.startsWith('@npub1')) { - const npub = word.replace('@', '').replace(/[^a-zA-Z0-9 ]/g, ''); - return word.replace(word, `~pub-${nip19.decode(npub).data}~`); - } - - // nostr account references - if (word.startsWith('nostr:npub1') || word.startsWith('npub1')) { - const npub = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); - return word.replace(word, `~pub-${nip19.decode(npub).data}~`); - } - - // nostr profile references - if (word.startsWith('nostr:nprofile1') || word.startsWith('nprofile1')) { - const nprofile = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); - const decoded = nip19.decode(nprofile).data as ProfilePointer; - return word.replace(word, `~pub-${decoded.pubkey}~`); - } - - // nostr account references - if (word.startsWith('nostr:note1') || word.startsWith('note1')) { - const note = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); - content.notes.push(nip19.decode(note).data as string); - return word.replace(word, ''); - } - - // nostr event references - if (word.startsWith('nostr:nevent1') || word.startsWith('nevent1')) { - const nevent = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); - const decoded = nip19.decode(nevent).data as EventPointer; - content.notes.push(decoded.id); - return word.replace(word, ''); - } - - // nostr address references - if (word.startsWith('nostr:naddr1') || word.startsWith('naddr1')) { - const naddr = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); - const decoded = nip19.decode(naddr).data as AddressPointer; - return word.replace(word, `~pub-${decoded.pubkey}~`); - } - - // lightning invoice - if (word.startsWith('lnbc') && word.length > 60) { - return word.replace(word, `~lnbc-${word}~`); - } - - // normal word - return word; - }); - - // update content with parsed version - content.parsed = parse.join(' '); - - return content; - } catch (e) { - console.error('cannot parse content, error: ', e); - } -} diff --git a/src/utils/parser.tsx b/src/utils/parser.tsx new file mode 100644 index 00000000..2441dcf7 --- /dev/null +++ b/src/utils/parser.tsx @@ -0,0 +1,173 @@ +import { nip19 } from 'nostr-tools'; +import { + AddressPointer, + EventPointer, + ProfilePointer, +} from 'nostr-tools/lib/types/nip19'; +import { Link } from 'react-router-dom'; +import reactStringReplace from 'react-string-replace'; + +import { Boost, Hashtag, Invoice, MentionUser } from '@shared/notes'; + +import { RichContent } from '@utils/types'; + +function isURL(string: string) { + try { + const url = new URL(string); + if (url.protocol.length > 0) { + if (url.protocol === 'https:' || url.protocol === 'http:') { + return true; + } else { + return false; + } + } + return true; + } catch (e) { + return false; + } +} + +export function parser(eventContent: string) { + const content: RichContent = { + parsed: null, + images: [], + videos: [], + links: [], + notes: [], + }; + + const parsed = eventContent.split(/\s/gm).map((word) => { + // nostr note references + if (word.startsWith('nostr:note1') || word.startsWith('note1')) { + const note = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); + content.notes.push(nip19.decode(note).data as string); + return word.replace(word, ' '); + } + + // nostr event references + if (word.startsWith('nostr:nevent1') || word.startsWith('nevent1')) { + const nevent = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); + const decoded = nip19.decode(nevent).data as EventPointer; + content.notes.push(decoded.id); + return word.replace(word, ' '); + } + + // url + if (isURL(word)) { + const url = new URL(word); + url.search = ''; + + if (url.pathname.match(/\.(jpg|jpeg|gif|png|webp|avif)$/)) { + // image url + content.images.push(word); + // remove url from original content + return word.replace(word, ' '); + } + + if (url.pathname.match(/\.(mp4|mov|webm|wmv|flv|mts|avi|ogv|mkv|mp3|m3u8)$/)) { + // video url + content.videos.push(word); + // remove url from original content + return word.replace(word, ' '); + } + + // normal url + if (content.links.length < 1) { + content.links.push(url.toString()); + return word.replace(word, ' '); + } else { + return reactStringReplace(word, word, (match, i) => ( + <> + {' '} + + {word} + {' '} + + )); + } + } + + // hashtag + if (word.startsWith('#') && word.length > 1) { + return reactStringReplace(word, word, (match, i) => ( + <> + {' '} + {' '} + + )); + } + + // boost + if (word.startsWith('$prism') && word.length > 1) { + return reactStringReplace(word, word, (match, i) => ( + <> + {' '} + {' '} + + )); + } + + // nostr account references (depreciated) + if (word.startsWith('@npub1')) { + const npub = word.replace('@', '').replace(/[^a-zA-Z0-9 ]/g, ''); + return reactStringReplace(word, word, (match, i) => ( + <> + {' '} + {' '} + + )); + } + + // nostr account references + if (word.startsWith('nostr:npub1') || word.startsWith('npub1')) { + const npub = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); + return reactStringReplace(word, word, (match, i) => ( + <> + {' '} + {' '} + + )); + } + + // nostr profile references + if (word.startsWith('nostr:nprofile1') || word.startsWith('nprofile1')) { + const nprofile = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); + const decoded = nip19.decode(nprofile).data as ProfilePointer; + return reactStringReplace(word, word, (match, i) => ( + <> + {' '} + {' '} + + )); + } + + // nostr address references + if (word.startsWith('nostr:naddr1') || word.startsWith('naddr1')) { + const naddr = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, ''); + const decoded = nip19.decode(naddr).data as AddressPointer; + return reactStringReplace(word, word, (match, i) => ( + <> + {' '} + {' '} + + )); + } + + // lightning invoice + if (word.startsWith('lnbc') && word.length > 60) { + return reactStringReplace(word, word, (match, i) => ( + <> + {' '} + {' '} + + )); + } + + // normal word + return ' ' + word + ' '; + }); + + // update content with parsed version + content.parsed = parsed; + return content; +} diff --git a/src/utils/types.d.ts b/src/utils/types.d.ts index 9c42afa2..a9e13585 100644 --- a/src/utils/types.d.ts +++ b/src/utils/types.d.ts @@ -1,8 +1,9 @@ import { type NDKEvent, type NDKUserProfile } from '@nostr-dev-kit/ndk'; import { type Response } from '@tauri-apps/plugin-http'; +import { ReactNode } from 'react'; export interface RichContent { - parsed: string; + parsed: string | ReactNode[]; images: string[]; videos: string[]; links: string[];