Browse Source

fixed markdown spacing

imwald
Silberengel 5 months ago
parent
commit
e05f43a1d8
  1. 99
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  2. 40
      src/components/Note/MarkdownArticle/remarkHashtags.ts

99
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 ImageWithLightbox from '@/components/ImageWithLightbox'
import MediaPlayer from '@/components/MediaPlayer' import MediaPlayer from '@/components/MediaPlayer'
import Wikilink from '@/components/UniversalContent/Wikilink' import Wikilink from '@/components/UniversalContent/Wikilink'
@ -15,6 +15,7 @@ import remarkMath from 'remark-math'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import NostrNode from './NostrNode' import NostrNode from './NostrNode'
import { remarkNostr } from './remarkNostr' import { remarkNostr } from './remarkNostr'
import { remarkHashtags } from './remarkHashtags'
import { Components } from './types' import { Components } from './types'
export default function MarkdownArticle({ export default function MarkdownArticle({
@ -27,6 +28,7 @@ export default function MarkdownArticle({
showImageGallery?: boolean showImageGallery?: boolean
}) { }) {
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { navigateToHashtag } = useSmartHashtagNavigation()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const contentRef = useRef<HTMLDivElement>(null) const contentRef = useRef<HTMLDivElement>(null)
@ -129,14 +131,28 @@ export default function MarkdownArticle({
// Normalize href to include leading slash if missing // Normalize href to include leading slash if missing
const normalizedHref = href.startsWith('/') ? href : `/${href}` 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 ( return (
<SecondaryPageLink <span
to={normalizedHref} className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer [&]:inline [&]:m-0 [&]:p-0 [&]:leading-normal"
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline" style={{ display: 'inline', margin: 0, padding: 0 }}
onClick={(e) => {
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} {children}
</SecondaryPageLink> </span>
) )
} }
@ -369,6 +385,15 @@ export default function MarkdownArticle({
.hljs-strong { .hljs-strong {
font-weight: bold; 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;
}
`}</style> `}</style>
<div <div
ref={contentRef} ref={contentRef}
@ -386,65 +411,9 @@ export default function MarkdownArticle({
className="w-full max-w-[400px] aspect-[3/1] object-cover my-0" className="w-full max-w-[400px] aspect-[3/1] object-cover my-0"
/> />
)} )}
<div className="break-words whitespace-pre-wrap"> <Markdown remarkPlugins={[remarkGfm, remarkMath, remarkNostr, remarkHashtags]} components={components}>
{event.content.split(/(#\w+|\[\[[^\]]+\]\])/).map((part, index, array) => { {event.content}
// Check if this part is a hashtag </Markdown>
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 <span key={`hashtag-plain-${index}`}>{part}</span>
}
// 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 (
<span key={`hashtag-wrapper-${index}`}>
{beforeSpace && beforeSpace}
<a
href={`/notes?t=${normalizedHashtag}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
const url = `/notes?t=${normalizedHashtag}`
console.log('[MarkdownArticle] Clicking hashtag, navigating to:', url)
push(url)
}}
>
{part}
</a>
{afterSpace && afterSpace}
</span>
)
}
// 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 <Wikilink key={`wikilink-${index}`} dTag={dtag} displayText={displayText} />
}
// Regular text
return <Markdown key={`text-${index}`} remarkPlugins={[remarkGfm, remarkMath, remarkNostr]} components={components}>{part}</Markdown>
})}
</div>
{/* Inline Media - Show for non-article content (kinds 1, 11, 1111) */} {/* Inline Media - Show for non-article content (kinds 1, 11, 1111) */}
{!showImageGallery && extractedMedia.videos.length > 0 && ( {!showImageGallery && extractedMedia.videos.length > 0 && (

40
src/components/Note/MarkdownArticle/remarkHashtags.ts

@ -23,11 +23,18 @@ export const remarkHashtags: Plugin<[], Root> = () => {
const hashtag = match[1] const hashtag = match[1]
// Add text before the hashtag // Add text before the hashtag
// Normalize whitespace to prevent paragraph breaks around hashtags
if (matchStart > lastIndex) { if (matchStart > lastIndex) {
children.push({ const beforeText = text.slice(lastIndex, matchStart)
type: 'text', // Replace ALL newlines with spaces to keep hashtags inline
value: text.slice(lastIndex, matchStart) // 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 // Create a link node for the hashtag
@ -46,15 +53,30 @@ export const remarkHashtags: Plugin<[], Root> = () => {
}) })
// Add remaining text after the last match // Add remaining text after the last match
// Normalize whitespace to prevent paragraph breaks
if (lastIndex < text.length) { if (lastIndex < text.length) {
children.push({ const afterText = text.slice(lastIndex)
type: 'text', // Replace ALL newlines with spaces to keep hashtags inline
value: text.slice(lastIndex) // 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 // Replace the text node with the processed children
parent.children.splice(index, 1, ...children) parent.children.splice(index, 1, ...filteredChildren)
}) })
} }
} }

Loading…
Cancel
Save