diff --git a/src/components/MarkdownContent/index.tsx b/src/components/MarkdownContent/index.tsx
index c5bf8dc..ebbe57e 100644
--- a/src/components/MarkdownContent/index.tsx
+++ b/src/components/MarkdownContent/index.tsx
@@ -1,13 +1,29 @@
import { SecondaryPageLink } from '@/PageManager'
+import { X_URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants'
import { toNote, toProfile } from '@/lib/link'
+import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
+import { EmbeddedHashtag, EmbeddedLNInvoice } from '../Embedded'
+import Emoji from '../Emoji'
+import ExternalLink from '../ExternalLink'
import ImageWithLightbox from '../ImageWithLightbox'
import NostrNode from '../Note/LongFormArticle/NostrNode'
import { remarkNostr } from '../Note/LongFormArticle/remarkNostr'
-import { Components } from '../Note/LongFormArticle/types'
+import { Components as BaseComponents } from '../Note/LongFormArticle/types'
+
+type InlineComponent = React.ComponentType<{ value: string }>
+
+interface Components extends BaseComponents {
+ hashtag: InlineComponent
+ emoji: InlineComponent
+ invoice: InlineComponent
+}
+import XEmbeddedPost from '../XEmbeddedPost'
+import YoutubeEmbeddedPlayer from '../YoutubeEmbeddedPlayer'
+import { remarkInlineContent } from './remarkInlineContent'
export default function MarkdownContent({
content,
@@ -16,10 +32,20 @@ export default function MarkdownContent({
content: string
event?: Event
}) {
+ const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event?.tags), [event?.tags])
+
const components = useMemo(
() =>
({
nostr: ({ rawText, bech32Id }) =>
{children}
, @@ -66,7 +90,6 @@ export default function MarkdownContent({{children}
),
code: ({ children, className }) => {
- // If inside a , render as block code (className contains language info)
if (className) {
return {children}
}
@@ -95,13 +118,13 @@ export default function MarkdownContent({
),
hr: () =>
}) as Components,
- [event?.pubkey]
+ [event?.pubkey, emojiInfos]
)
return (
{
if (url.startsWith('nostr:')) {
return url.slice(6)
diff --git a/src/components/MarkdownContent/remarkInlineContent.ts b/src/components/MarkdownContent/remarkInlineContent.ts
new file mode 100644
index 0000000..98275ae
--- /dev/null
+++ b/src/components/MarkdownContent/remarkInlineContent.ts
@@ -0,0 +1,74 @@
+import { EMOJI_SHORT_CODE_REGEX, HASHTAG_REGEX, LN_INVOICE_REGEX } from '@/constants'
+import type { PhrasingContent, Root, Text } from 'mdast'
+import type { Plugin } from 'unified'
+import type { Node } from 'unist'
+import { visit } from 'unist-util-visit'
+
+interface InlineContentNode extends Node {
+ type: 'hashtag' | 'emoji' | 'invoice'
+ data: {
+ hName: string
+ hProperties: { value: string }
+ }
+}
+
+const PATTERNS: { type: InlineContentNode['type']; regex: RegExp }[] = [
+ { type: 'invoice', regex: LN_INVOICE_REGEX },
+ { type: 'hashtag', regex: HASHTAG_REGEX },
+ { type: 'emoji', regex: EMOJI_SHORT_CODE_REGEX }
+]
+
+export const remarkInlineContent: Plugin<[], Root> = () => {
+ return (tree) => {
+ visit(tree, 'text', (node: Text, index, parent) => {
+ if (!parent || typeof index !== 'number') return
+
+ let segments: (Text | InlineContentNode)[] = [{ type: 'text', value: node.value }]
+
+ for (const { type, regex } of PATTERNS) {
+ const nextSegments: (Text | InlineContentNode)[] = []
+
+ for (const segment of segments) {
+ if (segment.type !== 'text') {
+ nextSegments.push(segment)
+ continue
+ }
+
+ const text = (segment as Text).value
+ const localRegex = new RegExp(regex.source, regex.flags)
+ const matches = Array.from(text.matchAll(localRegex))
+
+ if (matches.length === 0) {
+ nextSegments.push(segment)
+ continue
+ }
+
+ let lastIndex = 0
+ for (const match of matches) {
+ const matchStart = match.index!
+ if (matchStart > lastIndex) {
+ nextSegments.push({ type: 'text', value: text.slice(lastIndex, matchStart) })
+ }
+ nextSegments.push({
+ type,
+ data: {
+ hName: type,
+ hProperties: { value: match[0] }
+ }
+ } as InlineContentNode)
+ lastIndex = matchStart + match[0].length
+ }
+ if (lastIndex < text.length) {
+ nextSegments.push({ type: 'text', value: text.slice(lastIndex) })
+ }
+ }
+
+ segments = nextSegments
+ }
+
+ if (segments.length > 1 || (segments.length === 1 && segments[0].type !== 'text')) {
+ parent.children.splice(index, 1, ...(segments as PhrasingContent[]))
+ }
+ })
+ }
+}