**Motivations:** - Keep dependencies up to date for security and features - Automate dependency updates in deployment script - Fix compatibility issues with major version updates (React 19, Next.js 16, nostr-tools 2.x) **Root causes:** - Dependencies were outdated - Deployment script did not update dependencies before deploying - Major version updates introduced breaking API changes **Correctifs:** - Updated all dependencies to latest versions using npm-check-updates - Modified deploy.sh to run npm-check-updates before installing dependencies - Fixed nostr-tools 2.x API changes (generatePrivateKey -> generateSecretKey, signEvent -> finalizeEvent, verifySignature -> verifyEvent) - Fixed React 19 ref types to accept null - Fixed JSX namespace issues (JSX.Element -> React.ReactElement) - Added proper types for event callbacks - Fixed SimplePool.sub typing issues with type assertions **Evolutions:** - Deployment script now automatically updates dependencies to latest versions before deploying - All dependencies updated to latest versions (Next.js 14->16, React 18->19, nostr-tools 1->2, etc.) **Pages affectées:** - package.json - deploy.sh - lib/keyManagement.ts - lib/nostr.ts - lib/nostrRemoteSigner.ts - lib/zapVerification.ts - lib/platformTrackingEvents.ts - lib/sponsoringTracking.ts - lib/articlePublisherHelpersVerification.ts - lib/contentDeliveryVerification.ts - lib/paymentPollingZapReceipt.ts - lib/nostrPrivateMessages.ts - lib/nostrSubscription.ts - lib/nostrZapVerification.ts - lib/markdownRenderer.tsx - components/AuthorFilter.tsx - components/AuthorFilterButton.tsx - components/UserArticlesList.tsx - types/nostr-tools-extended.ts
236 lines
6.6 KiB
TypeScript
236 lines
6.6 KiB
TypeScript
import React from 'react'
|
|
import Link from 'next/link'
|
|
|
|
interface RenderState {
|
|
currentList: string[]
|
|
inCodeBlock: boolean
|
|
codeBlockContent: string[]
|
|
}
|
|
|
|
export function renderMarkdown(markdown: string): React.ReactElement[] {
|
|
const lines = markdown.split('\n')
|
|
const elements: React.ReactElement[] = []
|
|
const state: RenderState = {
|
|
currentList: [],
|
|
inCodeBlock: false,
|
|
codeBlockContent: [],
|
|
}
|
|
|
|
lines.forEach((line, index) => {
|
|
processLine(line, index, state, elements)
|
|
})
|
|
|
|
closeListIfNeeded('', lines.length, state, elements)
|
|
|
|
return elements
|
|
}
|
|
|
|
function processLine(line: string, index: number, state: RenderState, elements: React.ReactElement[]): void {
|
|
if (line.startsWith('```')) {
|
|
handleCodeBlock(line, index, state, elements)
|
|
return
|
|
}
|
|
|
|
if (state.inCodeBlock) {
|
|
state.codeBlockContent.push(line)
|
|
return
|
|
}
|
|
|
|
closeListIfNeeded(line, index, state, elements)
|
|
|
|
if (renderHeading(line, index, elements)) {
|
|
return
|
|
}
|
|
if (renderListLine(line, state)) {
|
|
return
|
|
}
|
|
if (renderLinkLine(line, index, elements)) {
|
|
return
|
|
}
|
|
if (renderBoldAndCodeLine(line, index, elements)) {
|
|
return
|
|
}
|
|
renderParagraphOrBreak(line, index, elements)
|
|
}
|
|
|
|
function renderHeading(line: string, index: number, elements: React.ReactElement[]): boolean {
|
|
if (line.startsWith('# ')) {
|
|
elements.push(<h1 key={index} className="text-3xl font-bold mt-8 mb-4 text-neon-cyan font-mono">{line.substring(2)}</h1>)
|
|
return true
|
|
}
|
|
if (line.startsWith('## ')) {
|
|
elements.push(<h2 key={index} className="text-2xl font-bold mt-6 mb-3 text-neon-cyan font-mono">{line.substring(3)}</h2>)
|
|
return true
|
|
}
|
|
if (line.startsWith('### ')) {
|
|
elements.push(<h3 key={index} className="text-xl font-semibold mt-4 mb-2 text-neon-cyan font-mono">{line.substring(4)}</h3>)
|
|
return true
|
|
}
|
|
if (line.startsWith('#### ')) {
|
|
elements.push(<h4 key={index} className="text-lg font-semibold mt-3 mb-2 text-neon-cyan font-mono">{line.substring(5)}</h4>)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function renderListLine(line: string, state: RenderState): boolean {
|
|
if (line.startsWith('- ') || line.startsWith('* ')) {
|
|
state.currentList.push(line)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function renderLinkLine(line: string, index: number, elements: React.ReactElement[]): boolean {
|
|
if (line.includes('[') && line.includes('](')) {
|
|
renderLink(line, index, elements)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function renderBoldAndCodeLine(line: string, index: number, elements: React.ReactElement[]): boolean {
|
|
if (line.includes('**') || line.includes('`')) {
|
|
renderBoldAndCode(line, index, elements)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function renderParagraphOrBreak(line: string, index: number, elements: React.ReactElement[]): void {
|
|
if (line.trim() !== '') {
|
|
elements.push(<p key={index} className="mb-4 text-cyber-accent">{line}</p>)
|
|
return
|
|
}
|
|
if (elements.length > 0) {
|
|
const last = elements[elements.length - 1] as { type?: unknown }
|
|
if (last?.type !== 'br') {
|
|
elements.push(<br key={`br-${index}`} />)
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleCodeBlock(
|
|
_line: string,
|
|
index: number,
|
|
state: RenderState,
|
|
elements: React.ReactElement[]
|
|
): void {
|
|
if (state.inCodeBlock) {
|
|
elements.push(
|
|
<pre key={`code-${index}`} className="bg-cyber-darker border border-neon-cyan/20 p-4 rounded-lg overflow-x-auto my-4 text-neon-cyan font-mono text-sm">
|
|
<code>{state.codeBlockContent.join('\n')}</code>
|
|
</pre>
|
|
)
|
|
state.codeBlockContent = []
|
|
state.inCodeBlock = false
|
|
} else {
|
|
state.inCodeBlock = true
|
|
}
|
|
}
|
|
|
|
function closeListIfNeeded(
|
|
line: string,
|
|
index: number,
|
|
state: RenderState,
|
|
elements: React.ReactElement[]
|
|
): void {
|
|
if (state.currentList.length > 0 && !line.startsWith('- ') && !line.startsWith('* ') && line.trim() !== '') {
|
|
elements.push(
|
|
<ul key={`list-${index}`} className="list-disc list-inside mb-4 space-y-1 text-cyber-accent marker:text-neon-cyan">
|
|
{state.currentList.map((item, i) => (
|
|
<li key={i} className="ml-4">{item.substring(2).trim()}</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
state.currentList = []
|
|
}
|
|
}
|
|
|
|
function createLinkElement(
|
|
text: string,
|
|
href: string,
|
|
key: string,
|
|
isExternal: boolean
|
|
): React.ReactElement {
|
|
const className = 'text-neon-green hover:text-neon-cyan underline transition-colors'
|
|
if (isExternal) {
|
|
return (
|
|
<a key={key} href={href} target="_blank" rel="noopener noreferrer" className={className}>
|
|
{text}
|
|
</a>
|
|
)
|
|
}
|
|
return (
|
|
<Link key={key} href={href} className={className}>
|
|
{text}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
function renderLink(line: string, index: number, elements: React.ReactElement[]): void {
|
|
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
|
|
let lastIndex = 0
|
|
const parts: (string | React.ReactElement)[] = []
|
|
let match
|
|
|
|
while ((match = linkRegex.exec(line)) !== null) {
|
|
if (!match[2]) {
|
|
continue
|
|
}
|
|
if (!match[1] || match.index > lastIndex) {
|
|
parts.push(line.substring(lastIndex, match.index))
|
|
}
|
|
const href = match[2]
|
|
const isExternal = href.startsWith('http')
|
|
if (match[1]) {
|
|
parts.push(createLinkElement(match[1], href, `link-${index}-${match.index}`, isExternal))
|
|
}
|
|
lastIndex = match.index + match[0].length
|
|
}
|
|
|
|
if (lastIndex < line.length) {
|
|
parts.push(line.substring(lastIndex))
|
|
}
|
|
|
|
elements.push(<p key={index} className="mb-4">{parts}</p>)
|
|
}
|
|
|
|
function renderBoldAndCode(line: string, index: number, elements: React.ReactElement[]): void {
|
|
const parts: (string | React.ReactElement)[] = []
|
|
const codeRegex = /`([^`]+)`/g
|
|
let codeMatch
|
|
let lastIndex = 0
|
|
|
|
while ((codeMatch = codeRegex.exec(line)) !== null) {
|
|
if (codeMatch.index > lastIndex) {
|
|
const beforeCode = line.substring(lastIndex, codeMatch.index)
|
|
processBold(beforeCode, parts)
|
|
}
|
|
parts.push(
|
|
<code key={`code-${codeMatch.index}`} className="bg-cyber-darker text-neon-cyan px-1 py-0.5 rounded text-sm font-mono border border-neon-cyan/20">
|
|
{codeMatch[1]}
|
|
</code>
|
|
)
|
|
lastIndex = codeMatch.index + codeMatch[0].length
|
|
}
|
|
|
|
if (lastIndex < line.length) {
|
|
const remaining = line.substring(lastIndex)
|
|
processBold(remaining, parts)
|
|
}
|
|
|
|
elements.push(<p key={index} className="mb-4">{parts.length > 0 ? parts : line}</p>)
|
|
}
|
|
|
|
function processBold(text: string, parts: (string | React.ReactElement)[]): void {
|
|
const boldParts = text.split(/(\*\*[^*]+\*\*)/g)
|
|
boldParts.forEach((part, i) => {
|
|
if (part.startsWith('**') && part.endsWith('**')) {
|
|
parts.push(<strong key={`bold-${i}`} className="text-neon-green font-semibold">{part.slice(2, -2)}</strong>)
|
|
} else if (part) {
|
|
parts.push(part)
|
|
}
|
|
})
|
|
}
|