add markdown support

This commit is contained in:
Ren Amamiya
2023-05-06 12:23:03 +07:00
parent 19796794b0
commit 9668b18a2f
9 changed files with 791 additions and 56 deletions

View File

@@ -1,3 +1,4 @@
import { ContentMarkdown } from '@lume/app/note/components/markdown';
import NoteMetadata from '@lume/app/note/components/metadata';
import { NoteParent } from '@lume/app/note/components/parent';
import { noteParser } from '@lume/app/note/components/parser';
@@ -32,9 +33,7 @@ export default function NoteBase({ event }: { event: any }) {
<div className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={event.pubkey} time={event.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{content.parsed}
</div>
<ContentMarkdown content={content.parsed} />
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
</div>

View File

@@ -0,0 +1,22 @@
import { NoteQuote } from '@lume/app/note/components/quote';
import { NoteMentionUser } from '@lume/app/note/components/user/mention';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
export const ContentMarkdown = ({ content }: { content: any }) => {
return (
<ReactMarkdown
remarkPlugins={[[remarkGfm]]}
linkTarget="_blank"
className="prose prose-zinc max-w-none break-words dark:prose-invert prose-p:text-[15px] prose-p:leading-tight prose-a:text-[15px] prose-a:leading-tight prose-a:text-fuchsia-500 prose-a:no-underline prose-a:hover:text-fuchsia-600 prose-a:hover:underline prose-ol:mb-1 prose-ul:mb-1 prose-li:text-[15px] prose-li:leading-tight"
components={{
h5: ({ ...props }) => <NoteMentionUser pubkey={props.content} />,
h6: ({ ...props }) => <NoteQuote id={props.content} />,
em: ({ ...props }) => <span className="text-fuchsia-500 hover:text-fuchsia-600" {...props} />,
}}
>
{content}
</ReactMarkdown>
);
};

View File

@@ -1,3 +1,4 @@
import { ContentMarkdown } from '@lume/app/note/components/markdown';
import NoteMetadata from '@lume/app/note/components/metadata';
import { noteParser } from '@lume/app/note/components/parser';
import ImagePreview from '@lume/app/note/components/preview/image';
@@ -71,9 +72,8 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
<div className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{content ? content.parsed : ''}
</div>
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100"></div>
<ContentMarkdown content={content.parsed} />
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
</div>

View File

@@ -1,8 +1,4 @@
import { NoteQuote } from '@lume/app/note/components/quote';
import { NoteMentionUser } from '@lume/app/note/components/user/mention';
import { Event, parseReferences } from 'nostr-tools';
import reactStringReplace from 'react-string-replace';
import { Event } from 'nostr-tools';
const getURLs = new RegExp(
'(^|[ \t\r\n])((ftp|http|https|gopher|mailto|news|nntp|telnet|wais|file|prospero|aim|webcal|wss|ws):(([A-Za-z0-9$_.+!*(),;/?:@&~=-])|%[A-Fa-f0-9]{2}){2,}(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*(),;/?:@&~=%-]*))?([A-Za-z0-9$_+!*();/?:~-]))',
@@ -10,7 +6,6 @@ const getURLs = new RegExp(
);
export const noteParser = (event: Event) => {
const references = parseReferences(event);
const content: { original: string; parsed: any; images: string[]; videos: string[] } = {
original: event.content,
parsed: event.content,
@@ -33,43 +28,22 @@ export const noteParser = (event: Event) => {
content.videos.push(url);
// remove url from original content
content.parsed = content.parsed.toString().replace(url, '');
} else {
content.parsed = reactStringReplace(content.parsed, url, () => {
return (
<a key={url} href={url} className="text-fuchsia-500 no-underline hover:text-fuchsia-600 hover:underline">
{url}
</a>
);
});
}
});
// handle hashtag
content.parsed = reactStringReplace(content.parsed, /#(\w+)/g, (match, i) => (
<span key={match + i} className="cursor-pointer text-fuchsia-500 hover:text-fuchsia-600">
#{match}
</span>
));
// handle note references
references?.forEach((reference) => {
if (reference?.profile) {
content.parsed = reactStringReplace(content.parsed, reference.text, () => {
return <NoteMentionUser key={reference.profile.pubkey} pubkey={reference.profile.pubkey} />;
});
}
if (reference?.event) {
content.parsed = reactStringReplace(content.parsed, reference.text, () => {
return <NoteQuote key={reference.event.id} id={reference.event.id} />;
});
}
// map hashtag to em
content.original.match(/#(\w+)(?!:\/\/)/gi)?.forEach((item) => {
content.parsed = content.parsed.replace(item, `*${item}*`);
});
// remove extra spaces
content.parsed.forEach((item, index) => {
if (typeof item === 'string') {
content.parsed[index] = item.replace(/\s{2,}/g, ' ');
}
// map note mention to h6
content.original.match(/^(nostr:)?(note1|nevent1).*$/gm)?.forEach((item) => {
content.parsed = content.parsed.replace(item, `###### ${item}`);
});
// map profile mention to h5
content.original.match(/^(nostr:)?(nprofile1|npub1).*$/gm)?.forEach((item) => {
content.parsed = content.parsed.replace(item, `##### ${item}`);
});
return content;

View File

@@ -1,3 +1,4 @@
import { ContentMarkdown } from '@lume/app/note/components/markdown';
import { noteParser } from '@lume/app/note/components/parser';
import ImagePreview from '@lume/app/note/components/preview/image';
import VideoPreview from '@lume/app/note/components/preview/video';
@@ -58,9 +59,7 @@ export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
<div className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{content ? content.parsed : ''}
</div>
<ContentMarkdown content={content.parsed} />
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
</div>

View File

@@ -1,4 +1,5 @@
import { noteParser } from '@lume/app/note/components//parser';
import { ContentMarkdown } from '@lume/app/note/components/markdown';
import ImagePreview from '@lume/app/note/components/preview/image';
import VideoPreview from '@lume/app/note/components/preview/video';
import NoteReplyUser from '@lume/app/note/components/user/reply';
@@ -11,7 +12,7 @@ export default function NoteReply({ data }: { data: any }) {
<div className="flex flex-col">
<NoteReplyUser pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-[17px] pl-[48px]">
<div className="whitespace-pre-line break-words text-sm leading-tight">{content.parsed}</div>
<ContentMarkdown content={content.parsed} />
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
</div>

View File

@@ -1,3 +1,4 @@
import { ContentMarkdown } from '@lume/app/note/components/markdown';
import NoteMetadata from '@lume/app/note/components/metadata';
import { noteParser } from '@lume/app/note/components/parser';
import ImagePreview from '@lume/app/note/components/preview/image';
@@ -64,9 +65,7 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
<div onClick={(e) => openNote(e)} className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={parseFallback.pubkey} time={parseFallback.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{contentFallback.parsed}
</div>
<ContentMarkdown content={contentFallback.parsed} />
{Array.isArray(contentFallback.images) && contentFallback.images.length ? (
<ImagePreview urls={contentFallback.images} />
) : (
@@ -114,9 +113,7 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
<div onClick={(e) => openNote(e)} className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{content ? content.parsed : ''}
</div>
<ContentMarkdown content={content.parsed} />
{Array.isArray(content.images) && content.images.length ? <ImagePreview urls={content.images} /> : <></>}
{Array.isArray(content.videos) && content.videos.length ? <VideoPreview urls={content.videos} /> : <></>}
</div>