147 lines
5.7 KiB
TypeScript
147 lines
5.7 KiB
TypeScript
import React, { useMemo } from 'react'
|
|
import { View, RichText, Video } from '@tarojs/components'
|
|
import { marked, Renderer } from 'marked'
|
|
import CodeBlock from './CodeBlock'
|
|
import './index.scss'
|
|
|
|
interface Props {
|
|
content: string
|
|
themeColor?: string
|
|
}
|
|
|
|
const MarkdownReader: React.FC<Props> = ({ content, themeColor = '#00b96b' }) => {
|
|
const elements = useMemo(() => {
|
|
if (!content) return []
|
|
|
|
const tokens = marked.lexer(content)
|
|
const result: React.ReactNode[] = []
|
|
let currentTokens: any[] = []
|
|
|
|
// Configure renderer
|
|
const renderer = new Renderer()
|
|
|
|
renderer.table = (header, body) => {
|
|
return `<div style="overflow-x: auto; width: 100%; -webkit-overflow-scrolling: touch;">
|
|
<table style="width: 100%; min-width: 600px; border-collapse: collapse; margin: 16px 0; font-size: 16px;">
|
|
<thead>${header}</thead>
|
|
<tbody>${body}</tbody>
|
|
</table>
|
|
</div>`
|
|
}
|
|
|
|
renderer.tablecell = (content, flags) => {
|
|
const type = flags.header ? 'th' : 'td'
|
|
const style = [
|
|
'border: 1px solid rgba(255,255,255,0.1)',
|
|
'padding: 10px',
|
|
flags.header ? 'background-color: rgba(255,255,255,0.05); font-weight: 700; color: #fff;' : 'color: #ddd;',
|
|
flags.align ? `text-align: ${flags.align}` : 'text-align: left'
|
|
].join(';')
|
|
return `<${type} style="${style}">${content}</${type}>`
|
|
}
|
|
|
|
renderer.image = (href, title, text) => {
|
|
return `<img src="${href}" style="max-width:100%;border-radius:8px;margin:10px 0;box-shadow: 0 4px 12px rgba(0,0,0,0.3);" title="${title || ''}" alt="${text || ''}" />`
|
|
}
|
|
|
|
renderer.link = (href, title, text) => {
|
|
return `<a href="${href}" style="color: ${themeColor}; text-decoration: none;">${text}</a>`
|
|
}
|
|
|
|
// Process tokens
|
|
tokens.forEach((token, index) => {
|
|
if (token.type === 'code') {
|
|
// Skip css blocks that look like the video component styles
|
|
if ((token.lang === 'css' || !token.lang) && token.text.includes('.simple-tech-video')) {
|
|
return
|
|
}
|
|
|
|
// Flush accumulated tokens
|
|
if (currentTokens.length > 0) {
|
|
// preserve links if any
|
|
(currentTokens as any).links = (tokens as any).links
|
|
const html = marked.parser(currentTokens as any, { renderer, breaks: true })
|
|
result.push(<RichText key={`rt-${index}`} nodes={html} className='markdown-text' />)
|
|
currentTokens = []
|
|
}
|
|
|
|
// Add code block
|
|
result.push(
|
|
<View key={`cb-${index}`} className='code-block-wrapper'>
|
|
<CodeBlock
|
|
code={token.text}
|
|
language={token.lang}
|
|
/>
|
|
</View>
|
|
)
|
|
} else if (token.type === 'html') {
|
|
// Check for video tag
|
|
const videoRegex = /<video[^>]*>[\s\S]*?<source[^>]*src=["'](.*?)["'][^>]*>[\s\S]*?<\/video>/i
|
|
const simpleVideoRegex = /<video[^>]*src=["'](.*?)["'][^>]*>/i
|
|
|
|
const match = token.text.match(videoRegex) || token.text.match(simpleVideoRegex)
|
|
|
|
if (match) {
|
|
// Flush accumulated tokens
|
|
if (currentTokens.length > 0) {
|
|
(currentTokens as any).links = (tokens as any).links
|
|
const html = marked.parser(currentTokens as any, { renderer, breaks: true })
|
|
result.push(<RichText key={`rt-${index}`} nodes={html} className='markdown-text' />)
|
|
currentTokens = []
|
|
}
|
|
|
|
const src = match[1]
|
|
// Try to extract caption
|
|
const captionRegex = /class=["']video-caption["'][^>]*>(.*?)<\/div>/i
|
|
const captionMatch = token.text.match(captionRegex)
|
|
const caption = captionMatch ? captionMatch[1] : null
|
|
|
|
result.push(
|
|
<View key={`video-${index}`} className='markdown-video-container'>
|
|
<Video
|
|
src={src}
|
|
className='markdown-video'
|
|
controls
|
|
autoplay={false}
|
|
objectFit='contain'
|
|
showFullscreenBtn
|
|
showPlayBtn
|
|
showCenterPlayBtn
|
|
enablePlayGesture
|
|
/>
|
|
{caption && <View className='markdown-video-caption'>{caption}</View>}
|
|
</View>
|
|
)
|
|
} else {
|
|
// Filter out style tags for video component if they are parsed as HTML
|
|
if (token.text.includes('<style>') && token.text.includes('.simple-tech-video')) {
|
|
// If it's JUST the style tag, ignore it. If it's mixed with other content, we might need to be careful.
|
|
// But usually marked parses block HTML separately.
|
|
// Let's verify if we can just skip it.
|
|
// If the token is ONLY the style block, we skip it.
|
|
// If it contains other content, we might need to strip the style.
|
|
// For now, let's assume it's a block HTML token.
|
|
return
|
|
}
|
|
currentTokens.push(token)
|
|
}
|
|
} else {
|
|
currentTokens.push(token)
|
|
}
|
|
})
|
|
|
|
// Flush remaining tokens
|
|
if (currentTokens.length > 0) {
|
|
(currentTokens as any).links = (tokens as any).links
|
|
const html = marked.parser(currentTokens as any, { renderer, breaks: true })
|
|
result.push(<RichText key={`rt-end`} nodes={html} className='markdown-text' />)
|
|
}
|
|
|
|
return result
|
|
}, [content, themeColor])
|
|
|
|
return <View className='markdown-reader'>{elements}</View>
|
|
}
|
|
|
|
export default MarkdownReader
|