diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx
index 81ceaa0..a50862f 100644
--- a/src/components/Content/index.tsx
+++ b/src/components/Content/index.tsx
@@ -10,6 +10,7 @@ import {
parseContent
} from '@/lib/content-parser'
import { getImetaInfosFromEvent } from '@/lib/event'
+import { containsMarkdown } from '@/lib/markdown'
import { getEmojiInfosFromEmojiTags, getImetaInfoFromImetaTag } from '@/lib/tag'
import { cn } from '@/lib/utils'
import mediaUpload from '@/services/media-upload.service'
@@ -27,6 +28,7 @@ import Emoji from '../Emoji'
import ExternalLink from '../ExternalLink'
import HighlightButton from '../HighlightButton'
import ImageGallery from '../ImageGallery'
+import MarkdownContent from '../MarkdownContent'
import MediaPlayer from '../MediaPlayer'
import PostEditor from '../PostEditor'
import WebPreview from '../WebPreview'
@@ -50,9 +52,11 @@ export default function Content({
const [showHighlightEditor, setShowHighlightEditor] = useState(false)
const [selectedText, setSelectedText] = useState('')
const translatedEvent = useTranslatedEvent(event?.id)
+ const resolvedContent = translatedEvent?.content ?? event?.content ?? content
+ const isMarkdown = useMemo(() => resolvedContent ? containsMarkdown(resolvedContent) : false, [resolvedContent])
const { nodes, allImages, lastNormalUrl, emojiInfos } = useMemo(() => {
- const _content = translatedEvent?.content ?? event?.content ?? content
- if (!_content) return {}
+ if (!resolvedContent || isMarkdown) return {}
+ const _content = resolvedContent
const nodes = parseContent(_content, [
EmbeddedEventParser,
@@ -96,17 +100,42 @@ export default function Content({
typeof lastNormalUrlNode?.data === 'string' ? lastNormalUrlNode.data : undefined
return { nodes, allImages, emojiInfos, lastNormalUrl }
- }, [event, translatedEvent, content])
-
- if (!nodes || nodes.length === 0) {
- return null
- }
+ }, [event, resolvedContent, isMarkdown])
const handleHighlight = (text: string) => {
setSelectedText(text)
setShowHighlightEditor(true)
}
+ if (!resolvedContent) {
+ return null
+ }
+
+ if (isMarkdown) {
+ return (
+ <>
+
+
+
+ {enableHighlight && (
+
+ )}
+ {enableHighlight && (
+
+ )}
+ >
+ )
+ }
+
+ if (!nodes || nodes.length === 0) {
+ return null
+ }
+
let imageIndex = 0
return (
<>
diff --git a/src/components/MarkdownContent/index.tsx b/src/components/MarkdownContent/index.tsx
new file mode 100644
index 0000000..c5bf8dc
--- /dev/null
+++ b/src/components/MarkdownContent/index.tsx
@@ -0,0 +1,117 @@
+import { SecondaryPageLink } from '@/PageManager'
+import { toNote, toProfile } from '@/lib/link'
+import { Event } from 'nostr-tools'
+import { useMemo } from 'react'
+import Markdown from 'react-markdown'
+import remarkGfm from 'remark-gfm'
+import ImageWithLightbox from '../ImageWithLightbox'
+import NostrNode from '../Note/LongFormArticle/NostrNode'
+import { remarkNostr } from '../Note/LongFormArticle/remarkNostr'
+import { Components } from '../Note/LongFormArticle/types'
+
+export default function MarkdownContent({
+ content,
+ event
+}: {
+ content: string
+ event?: Event
+}) {
+ const components = useMemo(
+ () =>
+ ({
+ nostr: ({ rawText, bech32Id }) => ,
+ a: ({ href, children }) => {
+ if (!href) return {children}
+ if (href.startsWith('note1') || href.startsWith('nevent1') || href.startsWith('naddr1')) {
+ return (
+
+ {children}
+
+ )
+ }
+ if (href.startsWith('npub1') || href.startsWith('nprofile1')) {
+ return (
+
+ {children}
+
+ )
+ }
+ return (
+ e.stopPropagation()}
+ >
+ {children}
+
+ )
+ },
+ h1: ({ children }) => {children}
,
+ h2: ({ children }) => {children}
,
+ h3: ({ children }) => {children}
,
+ h4: ({ children }) => {children}
,
+ h5: ({ children }) => {children}
,
+ h6: ({ children }) => {children}
,
+ p: ({ children }) => {children}
,
+ img: ({ src }) => (
+
+ ),
+ pre: ({ children }) => (
+ {children}
+ ),
+ code: ({ children, className }) => {
+ // If inside a , render as block code (className contains language info)
+ if (className) {
+ return {children}
+ }
+ return {children}
+ },
+ blockquote: ({ children }) => (
+
+ {children}
+
+ ),
+ ul: ({ children }) => ,
+ ol: ({ children }) => {children}
,
+ li: ({ children }) => {children},
+ table: ({ children }) => (
+
+ ),
+ th: ({ children }) => (
+
+ {children}
+ |
+ ),
+ td: ({ children }) => (
+ {children} |
+ ),
+ hr: () =>
+ }) as Components,
+ [event?.pubkey]
+ )
+
+ return (
+
+ {
+ if (url.startsWith('nostr:')) {
+ return url.slice(6)
+ }
+ return url
+ }}
+ components={components}
+ >
+ {content}
+
+
+ )
+}
diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts
new file mode 100644
index 0000000..6ce54bd
--- /dev/null
+++ b/src/lib/markdown.ts
@@ -0,0 +1,43 @@
+/**
+ * Detects whether a string contains meaningful Markdown formatting.
+ * Strips URLs and nostr: references first to avoid false positives.
+ */
+export function containsMarkdown(content: string): boolean {
+ // Remove URLs and nostr: references to avoid false positives
+ const cleaned = content
+ .replace(/https?:\/\/[^\s)>\]]+/g, '')
+ .replace(/nostr:[a-z0-9]+/g, '')
+
+ // Strong signals — any single one triggers markdown
+ const strongPatterns = [
+ /^```/m, // code fence
+ /\|[\s]*:?-+:?[\s]*\|/ // table separator |---|
+ ]
+
+ for (const pattern of strongPatterns) {
+ if (pattern.test(cleaned)) return true
+ }
+
+ // Medium signals — need 2+ different types
+ const mediumPatterns = [
+ /^#{1,6}\s+\S/m, // ATX heading (# text), not #hashtag
+ /\*\*[^*\n]+\*\*/, // bold **text**
+ /__[^_\n]+__/, // bold __text__
+ /\[[^\]]+\]\([^)]+\)/, // link [text](url)
+ /^>\s+\S/m, // blockquote > text
+ /^[-*]\s+\S/m, // unordered list - item or * item
+ /^\d+\.\s+\S/m, // ordered list 1. item
+ /^---$/m, // horizontal rule
+ /~~[^~\n]+~~/ // strikethrough ~~text~~
+ ]
+
+ let matchCount = 0
+ for (const pattern of mediumPatterns) {
+ if (pattern.test(cleaned)) {
+ matchCount++
+ if (matchCount >= 2) return true
+ }
+ }
+
+ return false
+}