Move the event parser and dedup functions to Rust (#206)

* feat: improve js parser

* feat: move parser and dedup to rust

* fix: parser

* fix: get event function

* feat: improve parser performance (#207)

* feat: improve parser performance

* feat: add test for video parsing

* feat: finish new parser

---------

Co-authored-by: XIAO YU <xyzmhx@gmail.com>
This commit is contained in:
雨宮蓮
2024-06-12 08:27:53 +07:00
committed by GitHub
parent 1c20512ecc
commit 71be59b2e9
21 changed files with 610 additions and 250 deletions

View File

@@ -1,4 +1,4 @@
import { NOSTR_EVENTS, NOSTR_MENTIONS, cn, parser } from "@lume/utils";
import { cn } from "@lume/utils";
import { type ReactNode, useMemo } from "react";
import reactStringReplace from "react-string-replace";
import { Hashtag } from "./mentions/hashtag";
@@ -21,54 +21,42 @@ export function NoteContent({
className?: string;
}) {
const event = useNoteContext();
const data = useMemo(() => {
const { content, images, videos } = parser(event.content);
const words = content.split(/( |\n)/);
const hashtags = words.filter((word) => word.startsWith("#"));
const events = words.filter((word) =>
NOSTR_EVENTS.some((el) => word.startsWith(el)),
);
const mentions = words.filter((word) =>
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
);
let richContent: ReactNode[] | string = content;
const content = useMemo(() => {
try {
if (hashtags.length) {
for (const hashtag of hashtags) {
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
richContent = reactStringReplace(richContent, regex, (_, index) => {
return <Hashtag key={hashtag + index} tag={hashtag} />;
});
// Get parsed meta
const { content, hashtags, events, mentions } = event.meta;
// Define rich content
let richContent: ReactNode[] | string = content;
for (const hashtag of hashtags) {
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
richContent = reactStringReplace(richContent, regex, (_, index) => {
return <Hashtag key={hashtag + index} tag={hashtag} />;
});
}
for (const event of events) {
if (quote) {
richContent = reactStringReplace(richContent, event, (_, index) => (
<MentionNote key={event + index} eventId={event} />
));
}
if (!quote && clean) {
richContent = reactStringReplace(richContent, event, () => null);
}
}
if (events.length) {
for (const event of events) {
if (quote) {
richContent = reactStringReplace(richContent, event, (_, index) => (
<MentionNote key={event + index} eventId={event} />
));
}
if (!quote && clean) {
richContent = reactStringReplace(richContent, event, () => null);
}
for (const user of mentions) {
if (mention) {
richContent = reactStringReplace(richContent, user, (_, index) => (
<MentionUser key={user + index} pubkey={user} />
));
}
}
if (mentions.length) {
for (const user of mentions) {
if (mention) {
richContent = reactStringReplace(richContent, user, (_, index) => (
<MentionUser key={user + index} pubkey={user} />
));
}
if (!mention && clean) {
richContent = reactStringReplace(richContent, user, () => null);
}
if (!mention && clean) {
richContent = reactStringReplace(richContent, user, () => null);
}
}
@@ -81,7 +69,7 @@ export function NoteContent({
href={match}
target="_blank"
rel="noreferrer"
className="line-clamp-1 text-blue-500 hover:text-blue-600"
className="text-blue-500 line-clamp-1 hover:text-blue-600"
>
{match}
</a>
@@ -92,25 +80,26 @@ export function NoteContent({
<div key={nanoid()} className="h-3" />
));
return { content: richContent, images, videos };
return richContent;
} catch (e) {
return { content, images, videos };
console.log("[parser]: ", e);
return event.content;
}
}, []);
}, [event.content]);
return (
<div className="flex flex-col gap-2">
<div
className={cn(
"select-text text-[15px] text-pretty content-break overflow-hidden",
event.content.length > 500 ? "max-h-[300px] gradient-mask-b-0" : "",
"select-text text-pretty content-break overflow-hidden",
event.content.length > 420 ? "max-h-[250px] gradient-mask-b-0" : "",
className,
)}
>
{data.content}
{content}
</div>
{data.images.length ? <Images urls={data.images} /> : null}
{data.videos.length ? <Videos urls={data.videos} /> : null}
{event.meta?.images.length ? <Images urls={event.meta.images} /> : null}
{event.meta?.videos.length ? <Videos urls={event.meta.videos} /> : null}
</div>
);
}

View File

@@ -1,13 +1,4 @@
import type { Settings } from "@lume/types";
import {
AUDIOS,
IMAGES,
NOSTR_EVENTS,
NOSTR_MENTIONS,
VIDEOS,
cn,
} from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { cn } from "@lume/utils";
import { nanoid } from "nanoid";
import { type ReactNode, useMemo } from "react";
import reactStringReplace from "react-string-replace";
@@ -19,135 +10,85 @@ import { VideoPreview } from "./preview/video";
import { useNoteContext } from "./provider";
export function NoteContentLarge({
compact = true,
className,
}: {
compact?: boolean;
className?: string;
}) {
const { settings }: { settings: Settings } = useRouteContext({
strict: false,
});
const event = useNoteContext();
const content = useMemo(() => {
const text = event.content.trim();
const words = text.split(/( |\n)/);
// @ts-ignore, kaboom !!!
let parsedContent: ReactNode[] = compact
? text.replace(/\n\s*\n/g, "\n")
: text;
const hashtags = words.filter((word) => word.startsWith("#"));
const events = words.filter((word) =>
NOSTR_EVENTS.some((el) => word.startsWith(el)),
);
const mentions = words.filter((word) =>
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
);
try {
if (hashtags.length) {
for (const hashtag of hashtags) {
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
parsedContent = reactStringReplace(parsedContent, regex, () => {
return <Hashtag key={nanoid()} tag={hashtag} />;
});
}
}
// Get parsed meta
const { images, videos, hashtags, events, mentions } = event.meta;
if (events.length) {
for (const event of events) {
parsedContent = reactStringReplace(
parsedContent,
event,
(match, i) => <MentionNote key={match + i} eventId={event} />,
);
}
}
// Define rich content
let richContent: ReactNode[] | string = event.content;
if (mentions.length) {
for (const mention of mentions) {
parsedContent = reactStringReplace(
parsedContent,
mention,
(match, i) => <MentionUser key={match + i} pubkey={mention} />,
);
}
}
parsedContent = reactStringReplace(
parsedContent,
/(https?:\/\/\S+)/gi,
(match, i) => {
try {
const url = new URL(match);
const ext = url.pathname.split(".")[1];
if (!settings.enhancedPrivacy) {
if (IMAGES.includes(ext)) {
return <ImagePreview key={match + i} url={url.toString()} />;
}
if (VIDEOS.includes(ext)) {
return <VideoPreview key={match + i} url={url.toString()} />;
}
if (AUDIOS.includes(ext)) {
return <VideoPreview key={match + i} url={url.toString()} />;
}
}
return (
<a
key={match + i}
href={match}
target="_blank"
rel="noreferrer"
className="content-break w-full font-normal text-blue-500 hover:text-blue-600"
>
{match}
</a>
);
} catch {
return (
<a
key={match + i}
href={match}
target="_blank"
rel="noreferrer"
className="content-break w-full font-normal text-blue-500 hover:text-blue-600"
>
{match}
</a>
);
}
},
);
if (compact) {
parsedContent = reactStringReplace(parsedContent, /\n*\n/g, () => (
<div key={nanoid()} className="h-1.5" />
for (const hashtag of hashtags) {
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
richContent = reactStringReplace(richContent, regex, () => (
<Hashtag key={nanoid()} tag={hashtag} />
));
}
parsedContent = reactStringReplace(
parsedContent,
/[\r]?\n[\r]?\n/g,
(_, index) => <br key={event.id + "_br_" + index} />,
for (const event of events) {
richContent = reactStringReplace(richContent, event, (match, i) => (
<MentionNote key={match + i} eventId={event} />
));
}
for (const mention of mentions) {
richContent = reactStringReplace(richContent, mention, (match, i) => (
<MentionUser key={match + i} pubkey={mention} />
));
}
for (const image of images) {
richContent = reactStringReplace(richContent, image, (match, i) => (
<ImagePreview key={match + i} url={match} />
));
}
for (const video of videos) {
richContent = reactStringReplace(richContent, video, (match, i) => (
<VideoPreview key={match + i} url={match} />
));
}
richContent = reactStringReplace(
richContent,
/(https?:\/\/\S+)/gi,
(match, i) => (
<a
key={match + i}
href={match}
target="_blank"
rel="noreferrer"
className="text-blue-500 line-clamp-1 hover:text-blue-600"
>
{match}
</a>
),
);
return parsedContent;
richContent = reactStringReplace(richContent, /(\r\n|\r|\n)+/g, () => (
<div key={nanoid()} className="h-3" />
));
return richContent;
} catch (e) {
return text;
console.log("[parser]: ", e);
return event.content;
}
}, []);
}, [event.content]);
return (
<div className={cn("select-text", className)}>
<div className="text-[15px] text-pretty content-break leading-normal">
{content}
</div>
<div
className={cn(
"select-text leading-normal text-pretty content-break",
className,
)}
>
{content}
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { Note } from "@/components/note";
import { User } from "@/components/user";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { NostrEvent } from "@lume/types";
import type { NostrEvent } from "@lume/types";
import { NostrQuery } from "@lume/system";
export function RepostNote({
@@ -21,12 +21,7 @@ export function RepostNote({
queryKey: ["repost", event.id],
queryFn: async () => {
try {
if (event.content.length > 50) {
const embed: NostrEvent = JSON.parse(event.content);
return embed;
}
const id = event.tags.find((el) => el[0] === "e")?.[1];
const id = event.tags.find((el) => el[0] === "e")[1];
const repostEvent = await NostrQuery.getEvent(id);
return repostEvent;
@@ -50,27 +45,27 @@ export function RepostNote({
<div className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
Reposted by
</div>
<User.Avatar className="size-6 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<User.Avatar className="object-cover rounded-full size-6 shrink-0 ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
</User.Root>
</User.Provider>
{isLoading ? (
<div className="flex h-20 items-center justify-center gap-2">
<div className="flex items-center justify-center h-20 gap-2">
<Spinner />
Loading event...
</div>
) : isError || !repostEvent ? (
<div className="flex h-20 items-center justify-center">
<div className="flex items-center justify-center h-20">
Event not found within your current relay set
</div>
) : (
<Note.Provider event={repostEvent}>
<Note.Root>
<div className="px-3 h-14 flex items-center justify-between">
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.Content className="px-3" />
<div className="mt-3 flex items-center gap-4 h-14 px-3">
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Open />
<Note.Reply />
<Note.Repost />