-
Notifications
You must be signed in to change notification settings - Fork 34
refactor: replace regex markdown renderer with react-markdown #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,128 +1,68 @@ | ||
| interface MarkdownRendererProps { | ||
| content: string | ||
| } | ||
| import ReactMarkdown from "react-markdown" | ||
| import type { Components } from "react-markdown" | ||
| import matter from "gray-matter" | ||
|
|
||
| // Conditionally set BASE_PATH based on environment | ||
| const BASE_PATH = process.env.NODE_ENV === "production" ? "/StableViewpoints" : "" | ||
|
|
||
| export function MarkdownRenderer({ content }: MarkdownRendererProps) { | ||
| // Enhanced markdown to HTML conversion | ||
| const renderMarkdown = (text: string) => { | ||
| let html = text | ||
|
|
||
| // Headers | ||
| html = html.replace(/^### (.*$)/gim, '<h3 class="text-xl font-medium text-gray-800 mt-5 mb-2">$1</h3>') | ||
| html = html.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-semibold text-gray-800 mt-6 mb-3">$1</h2>') | ||
| html = html.replace(/^# (.*$)/gim, '<h1 class="text-3xl font-bold text-gray-900 mt-8 mb-4">$1</h1>') | ||
|
|
||
| // Bold and italic | ||
| html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>") | ||
| html = html.replace(/\*(.*?)\*/g, "<em>$1</em>") | ||
|
|
||
| // Code blocks | ||
| html = html.replace( | ||
| /```([\s\S]*?)```/g, | ||
| '<pre class="bg-gray-100 p-4 rounded-lg overflow-x-auto my-4"><code class="text-sm">$1</code></pre>' | ||
| ) | ||
|
|
||
| // Inline code | ||
| html = html.replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-1 py-0.5 rounded text-sm">$1</code>') | ||
|
|
||
| // Images - handle base path for GitHub Pages | ||
| html = html.replace( | ||
| /!\[(.*?)\]\((.*?)\)/gim, | ||
| (match, alt, src) => { | ||
| // Add base path to image src if it starts with / | ||
| const imageSrc = src.startsWith('/') ? `${BASE_PATH}${src}` : src | ||
| return `<div class="my-8"><img src="${imageSrc}" alt="${alt}" class="rounded-lg w-full h-auto max-w-2xl mx-auto shadow-lg" /></div>` | ||
| } | ||
| ) | ||
|
|
||
| // Links | ||
| html = html.replace( | ||
| /\[(.*?)\]\((.*?)\)/gim, | ||
| '<a href="$2" target="_blank" rel="noopener noreferrer" class="text-[#228B22] hover:text-[#3E921E] underline">$1</a>', | ||
| ) | ||
|
|
||
| // Blockquotes | ||
| html = html.replace( | ||
| /^> (.*$)/gim, | ||
| '<blockquote class="border-l-4 border-[#FFBF00] pl-4 italic text-gray-600 my-6 bg-yellow-50 py-2">$1</blockquote>', | ||
| const components: Components = { | ||
| h1: ({ children }) => <h1 className="text-3xl font-bold text-gray-900 mt-8 mb-4">{children}</h1>, | ||
| h2: ({ children }) => <h2 className="text-2xl font-semibold text-gray-800 mt-6 mb-3">{children}</h2>, | ||
| h3: ({ children }) => <h3 className="text-xl font-medium text-gray-800 mt-5 mb-2">{children}</h3>, | ||
| h4: ({ children }) => <h4 className="text-lg font-medium text-gray-800 mt-4 mb-2">{children}</h4>, | ||
|
|
||
| p: ({ children }) => <p className="text-gray-700 leading-relaxed mb-4">{children}</p>, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find posts that will currently hit the invalid paragraph/image nesting path.
rg -n --glob 'public/articles/**/*.md' '^\s*!\[[^]]*\]\([^)]+\)\s*$|^\s*\[!\[[^]]*\]\([^)]+\)\]\([^)]+\)\s*$'Repository: StabilityNexus/StableViewpoints Length of output: 1784 🏁 Script executed: cat -n lib/markdown-renderer.tsxRepository: StabilityNexus/StableViewpoints Length of output: 3233 Remove the When markdown contains a standalone image (like Move the spacing to the Suggested fix img: ({ src, alt }) => {
const resolvedSrc = typeof src === "string" && src.startsWith("/") ? `${BASE_PATH}${src}` : src
- return (
- <div className="my-8">
- {/* eslint-disable-next-line `@next/next/no-img-element` */}
- <img src={resolvedSrc} alt={alt ?? ""} className="rounded-lg w-full h-auto max-w-2xl mx-auto shadow-lg" />
- </div>
- )
+ // eslint-disable-next-line `@next/next/no-img-element`
+ return <img src={resolvedSrc} alt={alt ?? ""} className="my-8 block w-full h-auto max-w-2xl mx-auto rounded-lg shadow-lg" />
},16 standalone images in your article files would be affected by this issue. 🤖 Prompt for AI Agents |
||
| strong: ({ children }) => <strong className="font-semibold text-gray-900">{children}</strong>, | ||
| em: ({ children }) => <em className="italic">{children}</em>, | ||
|
|
||
| ul: ({ children }) => <ul className="list-disc list-inside mb-4 text-gray-700 space-y-1">{children}</ul>, | ||
| ol: ({ children }) => <ol className="list-decimal list-inside mb-4 text-gray-700 space-y-1">{children}</ol>, | ||
| li: ({ children }) => <li className="mb-1">{children}</li>, | ||
|
|
||
| blockquote: ({ children }) => ( | ||
| <blockquote className="border-l-4 border-[#FFBF00] pl-4 italic text-gray-600 my-6 bg-yellow-50 py-2"> | ||
| {children} | ||
| </blockquote> | ||
| ), | ||
|
|
||
| hr: () => <hr className="my-8 border-gray-300" />, | ||
|
|
||
| code: ({ className, children, ...props }) => { | ||
| const isBlock = Boolean(className) | ||
| if (isBlock) return <code className={className}>{children}</code> | ||
|
bored-arvi marked this conversation as resolved.
|
||
| return <code className="bg-gray-100 px-1 py-0.5 rounded text-sm" {...props}>{children}</code> | ||
| }, | ||
|
Comment on lines
+29
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find fenced code blocks without an info string.
rg -n --glob 'public/articles/**/*.md' '^(```|~~~)[[:space:]]*$'Repository: StabilityNexus/StableViewpoints Length of output: 5052 Fix block code detection to handle unlabeled fenced blocks. Fenced code blocks without a language specifier (e.g., plain 🤖 Prompt for AI Agents |
||
|
|
||
| pre: ({ children }) => ( | ||
| <pre className="bg-gray-100 p-4 rounded-lg overflow-x-auto my-4 text-sm">{children}</pre> | ||
| ), | ||
|
|
||
| a: ({ href, children }) => ( | ||
| <a href={href} target="_blank" rel="noopener noreferrer" className="text-[#228B22] hover:text-[#3E921E] underline"> | ||
| {children} | ||
| </a> | ||
| ), | ||
|
|
||
| img: ({ src, alt }) => { | ||
| const resolvedSrc = typeof src === "string" && src.startsWith("/") ? `${BASE_PATH}${src}` : src | ||
| return ( | ||
| <div className="my-8"> | ||
| {/* eslint-disable-next-line @next/next/no-img-element */} | ||
| <img src={resolvedSrc} alt={alt ?? ""} className="rounded-lg w-full h-auto max-w-2xl mx-auto shadow-lg" /> | ||
| </div> | ||
| ) | ||
| }, | ||
| } | ||
|
|
||
| // Horizontal rules | ||
| html = html.replace(/^---$/gm, '<hr class="my-8 border-gray-300" />') | ||
|
|
||
| // Lists - improved list handling | ||
| const lines = html.split("\n") | ||
| let inList = false | ||
| let inOrderedList = false | ||
| const processedLines: string[] = [] | ||
|
|
||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i] | ||
| const trimmed = line.trim() | ||
|
|
||
| if (trimmed.startsWith("- ")) { | ||
| if (!inList) { | ||
| processedLines.push('<ul class="list-disc list-inside mb-4 text-gray-700 space-y-1">') | ||
| inList = true | ||
| } | ||
| processedLines.push(`<li class="mb-1">${trimmed.substring(2)}</li>`) | ||
| } else if (trimmed.match(/^\d+\. /)) { | ||
| if (!inOrderedList) { | ||
| processedLines.push('<ol class="list-decimal list-inside mb-4 text-gray-700 space-y-1">') | ||
| inOrderedList = true | ||
| } | ||
| processedLines.push(`<li class="mb-1">${trimmed.replace(/^\d+\. /, '')}</li>`) | ||
| } else { | ||
| if (inList) { | ||
| processedLines.push("</ul>") | ||
| inList = false | ||
| } | ||
| if (inOrderedList) { | ||
| processedLines.push("</ol>") | ||
| inOrderedList = false | ||
| } | ||
| processedLines.push(line) | ||
| } | ||
| } | ||
|
|
||
| if (inList) { | ||
| processedLines.push("</ul>") | ||
| } | ||
| if (inOrderedList) { | ||
| processedLines.push("</ol>") | ||
| } | ||
|
|
||
| html = processedLines.join("\n") | ||
|
|
||
| // Paragraphs | ||
| const finalLines = html.split("\n") | ||
| const finalProcessedLines = finalLines.map((line) => { | ||
| const trimmed = line.trim() | ||
| if (trimmed === "") return "" | ||
| if ( | ||
| trimmed.startsWith("<h") || | ||
| trimmed.startsWith("<blockquote") || | ||
| trimmed.startsWith("<ul") || | ||
| trimmed.startsWith("</ul>") || | ||
| trimmed.startsWith("<ol") || | ||
| trimmed.startsWith("</ol>") || | ||
| trimmed.startsWith("<li") || | ||
| trimmed.startsWith("<div") || | ||
| trimmed.startsWith("<img") || | ||
| trimmed.startsWith("<pre") || | ||
| trimmed.startsWith("<hr") || | ||
| trimmed.startsWith("<code") | ||
| ) { | ||
| return trimmed | ||
| } | ||
| return `<p class="text-gray-700 leading-relaxed mb-4">${trimmed}</p>` | ||
| }) | ||
|
|
||
| return finalProcessedLines.join("\n") | ||
| } | ||
|
|
||
| return <div className="prose prose-lg max-w-none" dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }} /> | ||
| interface MarkdownRendererProps { | ||
| content: string | ||
| } | ||
|
|
||
| export function MarkdownRenderer({ content }: MarkdownRendererProps) { | ||
| const { content: body } = matter(content) | ||
|
|
||
|
bored-arvi marked this conversation as resolved.
|
||
| return ( | ||
| <div className="prose prose-lg max-w-none"> | ||
| <ReactMarkdown components={components}>{body}</ReactMarkdown> | ||
| </div> | ||
|
bored-arvi marked this conversation as resolved.
|
||
| ) | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 1037
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 1290
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 156
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 617
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 3233
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 141
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 234
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 108
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 255
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 340
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 1294
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 121
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 135
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 57
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 228
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 2349
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 128
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 750
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 708
🏁 Script executed:
Repository: StabilityNexus/StableViewpoints
Length of output: 158
Remove the redundant
gray-matterparse and import.Content is already parsed in
lib/blog-server.ts:18-36bygetPostBySlugServer(), which extracts and returns only the body. Parsing again in the renderer violates NextJS client-rendering best practices sincegray-matteris a Node.js library, adds unnecessary render-time work, and can incorrectly re-parse a legitimate leading---in the article body.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents