diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index aca7cec..1eed75c 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -1,4 +1,4 @@ -import { SecondaryPageLink, useSecondaryPage } from '@/PageManager' +import { SecondaryPageLink, useSecondaryPage, useSmartHashtagNavigation } from '@/PageManager' import ImageWithLightbox from '@/components/ImageWithLightbox' import MediaPlayer from '@/components/MediaPlayer' import Wikilink from '@/components/UniversalContent/Wikilink' @@ -15,6 +15,7 @@ import remarkMath from 'remark-math' import 'katex/dist/katex.min.css' import NostrNode from './NostrNode' import { remarkNostr } from './remarkNostr' +import { remarkHashtags } from './remarkHashtags' import { Components } from './types' export default function MarkdownArticle({ @@ -27,6 +28,7 @@ export default function MarkdownArticle({ showImageGallery?: boolean }) { const { push } = useSecondaryPage() + const { navigateToHashtag } = useSmartHashtagNavigation() const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const contentRef = useRef(null) @@ -129,14 +131,28 @@ export default function MarkdownArticle({ // Normalize href to include leading slash if missing const normalizedHref = href.startsWith('/') ? href : `/${href}` - // Inline hashtags from content should always be green + // Render hashtags as inline span elements - force inline display with no margins return ( - { + e.stopPropagation() + e.preventDefault() + navigateToHashtag(normalizedHref) + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation() + e.preventDefault() + navigateToHashtag(normalizedHref) + } + }} + role="button" + tabIndex={0} > {children} - + ) } @@ -369,6 +385,15 @@ export default function MarkdownArticle({ .hljs-strong { font-weight: bold; } + /* Force hashtag links to stay inline - override prose styles */ + .prose a[href^="/notes?t="], + .prose a[href^="notes?t="], + .prose span[role="button"][tabindex="0"] { + display: inline !important; + margin: 0 !important; + padding: 0 !important; + line-height: inherit !important; + } `}
)} -
- {event.content.split(/(#\w+|\[\[[^\]]+\]\])/).map((part, index, array) => { - // Check if this part is a hashtag - if (part.match(/^#\w+$/)) { - const hashtag = part.slice(1) - const normalizedHashtag = hashtag.toLowerCase() - - // Only render as green link if this hashtag is actually in the content - if (!contentHashtags.has(normalizedHashtag)) { - // Hashtag not in content, render as plain text - return {part} - } - - // Add spaces before and after unless at start/end of line - const isStartOfLine = index === 0 || array[index - 1].match(/^[\s]*$/) !== null - const isEndOfLine = index === array.length - 1 || array[index + 1].match(/^[\s]*$/) !== null - - const beforeSpace = isStartOfLine ? '' : ' ' - const afterSpace = isEndOfLine ? '' : ' ' - - // Inline hashtags from content should always be green - return ( - - {beforeSpace && beforeSpace} - { - e.preventDefault() - e.stopPropagation() - const url = `/notes?t=${normalizedHashtag}` - console.log('[MarkdownArticle] Clicking hashtag, navigating to:', url) - push(url) - }} - > - {part} - - {afterSpace && afterSpace} - - ) - } - // Check if this part is a wikilink - if (part.match(/^\[\[([^\]]+)\]\]$/)) { - const content = part.slice(2, -2) - let target = content.includes('|') ? content.split('|')[0].trim() : content.trim() - let displayText = content.includes('|') ? content.split('|')[1].trim() : content.trim() - - if (content.startsWith('book:')) { - target = content.replace('book:', '').trim() - } - - const dtag = target.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') - - return - } - // Regular text - return {part} - })} -
+ + {event.content} + {/* Inline Media - Show for non-article content (kinds 1, 11, 1111) */} {!showImageGallery && extractedMedia.videos.length > 0 && ( diff --git a/src/components/Note/MarkdownArticle/remarkHashtags.ts b/src/components/Note/MarkdownArticle/remarkHashtags.ts index a249345..8e1c250 100644 --- a/src/components/Note/MarkdownArticle/remarkHashtags.ts +++ b/src/components/Note/MarkdownArticle/remarkHashtags.ts @@ -23,11 +23,18 @@ export const remarkHashtags: Plugin<[], Root> = () => { const hashtag = match[1] // Add text before the hashtag + // Normalize whitespace to prevent paragraph breaks around hashtags if (matchStart > lastIndex) { - children.push({ - type: 'text', - value: text.slice(lastIndex, matchStart) - }) + const beforeText = text.slice(lastIndex, matchStart) + // Replace ALL newlines with spaces to keep hashtags inline + // This prevents markdown from treating newlines as paragraph breaks + const normalized = beforeText.replace(/\s*\n+\s*/g, ' ') + if (normalized.trim()) { + children.push({ + type: 'text', + value: normalized + }) + } } // Create a link node for the hashtag @@ -46,15 +53,30 @@ export const remarkHashtags: Plugin<[], Root> = () => { }) // Add remaining text after the last match + // Normalize whitespace to prevent paragraph breaks if (lastIndex < text.length) { - children.push({ - type: 'text', - value: text.slice(lastIndex) - }) + const afterText = text.slice(lastIndex) + // Replace ALL newlines with spaces to keep hashtags inline + // This prevents markdown from treating newlines as paragraph breaks + const normalized = afterText.replace(/\s*\n+\s*/g, ' ') + if (normalized.trim()) { + children.push({ + type: 'text', + value: normalized + }) + } } + // Filter out empty text nodes to prevent paragraph breaks + const filteredChildren = children.filter((child) => { + if (child.type === 'text') { + return child.value.trim().length > 0 + } + return true + }) + // Replace the text node with the processed children - parent.children.splice(index, 1, ...children) + parent.children.splice(index, 1, ...filteredChildren) }) } }