From 3a4dfe720089b54d0d499fa2c24f4827a127b320 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 11 Nov 2025 12:40:19 +0100 Subject: [PATCH] fixed images rendering as markup --- .../Note/MarkdownArticle/MarkdownArticle.tsx | 269 +++++++++++++++--- 1 file changed, 233 insertions(+), 36 deletions(-) diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index 59dc637..cceacc8 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -874,27 +874,121 @@ function parseMarkdownContent( const paragraphs = text.split(/\n\n+/) paragraphs.forEach((paragraph, paraIdx) => { - // Convert single newlines to spaces within the paragraph - // This prevents hard breaks within sentences - // Also collapse multiple spaces into one - let normalizedPara = paragraph.replace(/\n/g, ' ') - // Collapse multiple consecutive spaces/tabs (2+) into a single space, but preserve single spaces - normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ') - // Trim only leading/trailing whitespace, not internal spaces - normalizedPara = normalizedPara.trim() - if (normalizedPara) { - // Process paragraph for inline formatting (which will handle markdown links) - const paraContent = parseInlineMarkdown(normalizedPara, `text-${patternIdx}-para-${paraIdx}`, footnotes) - // Wrap in paragraph tag (no whitespace-pre-wrap, let normal text wrapping handle it) - parts.push( -

- {paraContent} -

- ) - } else if (paraIdx > 0) { - // Empty paragraph between non-empty paragraphs - add spacing - // This handles cases where there are multiple consecutive newlines - parts.push(
) + // Check for markdown images in this paragraph and extract them + const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g + const imageMatches = Array.from(paragraph.matchAll(markdownImageRegex)) + + if (imageMatches.length > 0) { + // Process text and images separately + let paraLastIndex = 0 + imageMatches.forEach((match, imgIdx) => { + if (match.index !== undefined) { + const imgStart = match.index + const imgEnd = match.index + match[0].length + const imgUrl = match[2] + const cleaned = cleanUrl(imgUrl) + + // Add text before this image + if (imgStart > paraLastIndex) { + const textBefore = paragraph.slice(paraLastIndex, imgStart) + let normalizedText = textBefore.replace(/\n/g, ' ') + normalizedText = normalizedText.replace(/[ \t]{2,}/g, ' ') + normalizedText = normalizedText.trim() + if (normalizedText) { + const textContent = parseInlineMarkdown(normalizedText, `text-${patternIdx}-para-${paraIdx}-img-${imgIdx}`, footnotes) + parts.push( +

+ {textContent} +

+ ) + } + } + + // Render the image + if (isImage(cleaned)) { + let imageIndex = imageIndexMap.get(cleaned) + if (imageIndex === undefined && getImageIdentifier) { + const identifier = getImageIdentifier(cleaned) + if (identifier) { + imageIndex = imageIndexMap.get(`__img_id:${identifier}`) + } + } + + let thumbnailUrl: string | undefined + if (imageThumbnailMap) { + thumbnailUrl = imageThumbnailMap.get(cleaned) + if (!thumbnailUrl && getImageIdentifier) { + const identifier = getImageIdentifier(cleaned) + if (identifier) { + thumbnailUrl = imageThumbnailMap.get(`__img_id:${identifier}`) + } + } + } + const displayUrl = thumbnailUrl || imgUrl + + parts.push( +
+ { + e.stopPropagation() + if (imageIndex !== undefined) { + openLightbox(imageIndex) + } + }} + /> +
+ ) + } + + paraLastIndex = imgEnd + } + }) + + // Add any remaining text after the last image + if (paraLastIndex < paragraph.length) { + const remainingText = paragraph.slice(paraLastIndex) + let normalizedText = remainingText.replace(/\n/g, ' ') + normalizedText = normalizedText.replace(/[ \t]{2,}/g, ' ') + normalizedText = normalizedText.trim() + if (normalizedText) { + const textContent = parseInlineMarkdown(normalizedText, `text-${patternIdx}-para-${paraIdx}-final`, footnotes) + parts.push( +

+ {textContent} +

+ ) + } + } + } else { + // No images, process normally + // Convert single newlines to spaces within the paragraph + // This prevents hard breaks within sentences + // Also collapse multiple spaces into one + let normalizedPara = paragraph.replace(/\n/g, ' ') + // Collapse multiple consecutive spaces/tabs (2+) into a single space, but preserve single spaces + normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ') + // Trim only leading/trailing whitespace, not internal spaces + normalizedPara = normalizedPara.trim() + if (normalizedPara) { + // Process paragraph for inline formatting (which will handle markdown links) + const paraContent = parseInlineMarkdown(normalizedPara, `text-${patternIdx}-para-${paraIdx}`, footnotes) + // Wrap in paragraph tag (no whitespace-pre-wrap, let normal text wrapping handle it) + parts.push( +

+ {paraContent} +

+ ) + } else if (paraIdx > 0) { + // Empty paragraph between non-empty paragraphs - add spacing + // This handles cases where there are multiple consecutive newlines + parts.push(
) + } } }) @@ -1390,23 +1484,126 @@ function parseMarkdownContent( lastIndex < p.end ) if (!isInTable && text.trim()) { - // Split remaining text into paragraphs - const paragraphs = text.split(/\n\n+/) - paragraphs.forEach((paragraph, paraIdx) => { - // Convert single newlines to spaces within the paragraph - // Collapse multiple consecutive spaces/tabs (2+) into a single space, but preserve single spaces - let normalizedPara = paragraph.replace(/\n/g, ' ') - normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ') - normalizedPara = normalizedPara.trim() - if (normalizedPara) { - const paraContent = parseInlineMarkdown(normalizedPara, `text-end-para-${paraIdx}`, footnotes) - parts.push( -

- {paraContent} -

- ) + // Check if there are any markdown images in the remaining text that weren't detected as patterns + // If so, we need to process them separately before processing the text + const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g + const remainingImageMatches = Array.from(text.matchAll(markdownImageRegex)) + + // Process images first, then text between/after them + let textLastIndex = 0 + remainingImageMatches.forEach((match, imgIdx) => { + if (match.index !== undefined) { + const imgStart = match.index + const imgEnd = match.index + match[0].length + const imgUrl = match[2] + const cleaned = cleanUrl(imgUrl) + + // Add text before this image + if (imgStart > textLastIndex) { + const textBefore = text.slice(textLastIndex, imgStart).trim() + if (textBefore) { + // Split into paragraphs + const paragraphs = textBefore.split(/\n\n+/) + paragraphs.forEach((paragraph, paraIdx) => { + let normalizedPara = paragraph.replace(/\n/g, ' ') + normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ') + normalizedPara = normalizedPara.trim() + if (normalizedPara) { + const paraContent = parseInlineMarkdown(normalizedPara, `text-end-para-${imgIdx}-${paraIdx}`, footnotes) + parts.push( +

+ {paraContent} +

+ ) + } + }) + } + } + + // Render the image + if (isImage(cleaned)) { + let imageIndex = imageIndexMap.get(cleaned) + if (imageIndex === undefined && getImageIdentifier) { + const identifier = getImageIdentifier(cleaned) + if (identifier) { + imageIndex = imageIndexMap.get(`__img_id:${identifier}`) + } + } + + let thumbnailUrl: string | undefined + if (imageThumbnailMap) { + thumbnailUrl = imageThumbnailMap.get(cleaned) + if (!thumbnailUrl && getImageIdentifier) { + const identifier = getImageIdentifier(cleaned) + if (identifier) { + thumbnailUrl = imageThumbnailMap.get(`__img_id:${identifier}`) + } + } + } + const displayUrl = thumbnailUrl || imgUrl + + parts.push( +
+ { + e.stopPropagation() + if (imageIndex !== undefined) { + openLightbox(imageIndex) + } + }} + /> +
+ ) + } + + textLastIndex = imgEnd } }) + + // Add any remaining text after the last image + if (textLastIndex < text.length) { + const remainingText = text.slice(textLastIndex).trim() + if (remainingText) { + const paragraphs = remainingText.split(/\n\n+/) + paragraphs.forEach((paragraph, paraIdx) => { + let normalizedPara = paragraph.replace(/\n/g, ' ') + normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ') + normalizedPara = normalizedPara.trim() + if (normalizedPara) { + const paraContent = parseInlineMarkdown(normalizedPara, `text-end-final-para-${paraIdx}`, footnotes) + parts.push( +

+ {paraContent} +

+ ) + } + }) + } + } else if (remainingImageMatches.length === 0) { + // No images found, process the text normally + const paragraphs = text.split(/\n\n+/) + paragraphs.forEach((paragraph, paraIdx) => { + // Convert single newlines to spaces within the paragraph + // Collapse multiple consecutive spaces/tabs (2+) into a single space, but preserve single spaces + let normalizedPara = paragraph.replace(/\n/g, ' ') + normalizedPara = normalizedPara.replace(/[ \t]{2,}/g, ' ') + normalizedPara = normalizedPara.trim() + if (normalizedPara) { + const paraContent = parseInlineMarkdown(normalizedPara, `text-end-para-${paraIdx}`, footnotes) + parts.push( +

+ {paraContent} +

+ ) + } + }) + } } } }