mirror of
https://github.com/dergigi/boris.git
synced 2026-01-18 22:34:34 +01:00
- Replace entire <video>...</video> and <img> tags with placeholders - Extract URLs in same order to align with placeholders - Also replace bare file URLs and platform-classified video URLs - Ensures no broken tags remain; uses ReactPlayer for rendering
213 lines
7.5 KiB
TypeScript
213 lines
7.5 KiB
TypeScript
import React, { useMemo, forwardRef } from 'react'
|
|
import ReactPlayer from 'react-player'
|
|
import { classifyUrl } from '../utils/helpers'
|
|
|
|
interface VideoEmbedProcessorProps {
|
|
html: string
|
|
renderVideoLinksAsEmbeds: boolean
|
|
className?: string
|
|
onMouseUp?: (e: React.MouseEvent) => void
|
|
onTouchEnd?: (e: React.TouchEvent) => void
|
|
}
|
|
|
|
/**
|
|
* Component that processes HTML content and optionally embeds video links
|
|
* as ReactPlayer components when renderVideoLinksAsEmbeds is enabled
|
|
*/
|
|
const VideoEmbedProcessor = forwardRef<HTMLDivElement, VideoEmbedProcessorProps>(({
|
|
html,
|
|
renderVideoLinksAsEmbeds,
|
|
className,
|
|
onMouseUp,
|
|
onTouchEnd
|
|
}, ref) => {
|
|
const processedHtml = useMemo(() => {
|
|
if (!renderVideoLinksAsEmbeds || !html) {
|
|
return html
|
|
}
|
|
|
|
// Process HTML in stages: <video> blocks, <img> tags with video src, and bare video URLs
|
|
let result = html
|
|
|
|
const collectedUrls: string[] = []
|
|
let placeholderIndex = 0
|
|
|
|
// 1) Replace entire <video>...</video> blocks when they reference a video URL
|
|
const videoBlockPattern = /<video[^>]*>[\s\S]*?<\/video>/gi
|
|
const videoBlocks = result.match(videoBlockPattern) || []
|
|
videoBlocks.forEach((block) => {
|
|
// Try src on <video>
|
|
let url: string | null = null
|
|
const videoSrcMatch = block.match(/<video[^>]*\s+src=["']?(https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)[^\s<>"']*)["']?[^>]*>/i)
|
|
if (videoSrcMatch && videoSrcMatch[1]) {
|
|
url = videoSrcMatch[1]
|
|
} else {
|
|
// Try nested <source>
|
|
const sourceSrcMatch = block.match(/<source[^>]*\s+src=["']?(https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)[^\s<>"']*)["']?[^>]*>/i)
|
|
if (sourceSrcMatch && sourceSrcMatch[1]) {
|
|
url = sourceSrcMatch[1]
|
|
}
|
|
}
|
|
if (url) {
|
|
collectedUrls.push(url)
|
|
const placeholder = `__VIDEO_EMBED_${placeholderIndex}__`
|
|
const escaped = block.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
result = result.replace(new RegExp(escaped, 'g'), placeholder)
|
|
placeholderIndex++
|
|
}
|
|
})
|
|
|
|
// 2) Replace entire <img ...> tags if their src points to a video
|
|
const imgTagPattern = /<img[^>]*>/gi
|
|
const allImgTags = result.match(imgTagPattern) || []
|
|
allImgTags.forEach((imgTag) => {
|
|
const srcMatch = imgTag.match(/src=["']?(https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)[^\s<>"']*)["']?/i)
|
|
if (srcMatch && srcMatch[1]) {
|
|
const videoUrl = srcMatch[1]
|
|
collectedUrls.push(videoUrl)
|
|
const placeholder = `__VIDEO_EMBED_${placeholderIndex}__`
|
|
const escapedTag = imgTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
result = result.replace(new RegExp(escapedTag, 'g'), placeholder)
|
|
placeholderIndex++
|
|
}
|
|
})
|
|
|
|
// 3) Replace remaining bare video URLs (direct files or recognized video platforms)
|
|
const fileVideoPattern = /https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)(?:\?[^\s<>"']*)?/gi
|
|
const fileVideoUrls: string[] = result.match(fileVideoPattern) || []
|
|
|
|
const allUrlPattern = /https?:\/\/[^\s<>"']+(?=\s|>|"|'|$)/gi
|
|
const allUrls: string[] = result.match(allUrlPattern) || []
|
|
const platformVideoUrls = allUrls.filter(url => {
|
|
// include URLs classified as video and not already collected
|
|
const classification = classifyUrl(url)
|
|
return classification.type === 'video' && !collectedUrls.includes(url)
|
|
})
|
|
|
|
const remainingUrls = [...fileVideoUrls, ...platformVideoUrls].filter(url => !collectedUrls.includes(url))
|
|
|
|
let processedHtml = result
|
|
remainingUrls.forEach((url) => {
|
|
const placeholder = `__VIDEO_EMBED_${placeholderIndex}__`
|
|
processedHtml = processedHtml.replace(new RegExp(url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), placeholder)
|
|
collectedUrls.push(url)
|
|
placeholderIndex++
|
|
})
|
|
|
|
// If nothing collected, return original html
|
|
if (collectedUrls.length === 0) {
|
|
return html
|
|
}
|
|
|
|
return processedHtml
|
|
}, [html, renderVideoLinksAsEmbeds])
|
|
|
|
const videoUrls = useMemo(() => {
|
|
if (!renderVideoLinksAsEmbeds || !html) {
|
|
return []
|
|
}
|
|
|
|
const urls: string[] = []
|
|
|
|
// 1) Extract from <video> blocks first (video src or nested source src)
|
|
const videoBlockPattern = /<video[^>]*>[\s\S]*?<\/video>/gi
|
|
const videoBlocks = html.match(videoBlockPattern) || []
|
|
videoBlocks.forEach((block) => {
|
|
let url: string | null = null
|
|
const videoSrcMatch = block.match(/<video[^>]*\s+src=["']?(https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)[^\s<>"']*)["']?[^>]*>/i)
|
|
if (videoSrcMatch && videoSrcMatch[1]) {
|
|
url = videoSrcMatch[1]
|
|
} else {
|
|
const sourceSrcMatch = block.match(/<source[^>]*\s+src=["']?(https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)[^\s<>"']*)["']?[^>]*>/i)
|
|
if (sourceSrcMatch && sourceSrcMatch[1]) {
|
|
url = sourceSrcMatch[1]
|
|
}
|
|
}
|
|
if (url && !urls.includes(url)) urls.push(url)
|
|
})
|
|
|
|
// 2) Extract from <img> tags with video src
|
|
const imgTagPattern = /<img[^>]*>/gi
|
|
const allImgTags = html.match(imgTagPattern) || []
|
|
allImgTags.forEach((imgTag) => {
|
|
const srcMatch = imgTag.match(/src=["']?(https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)[^\s<>"']*)["']?/i)
|
|
if (srcMatch && srcMatch[1] && !urls.includes(srcMatch[1])) {
|
|
urls.push(srcMatch[1])
|
|
}
|
|
})
|
|
|
|
// 3) Extract remaining direct file URLs and platform-classified video URLs
|
|
const fileVideoPattern = /https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)(?:\?[^\s<>"']*)?/gi
|
|
const fileVideoUrls: string[] = html.match(fileVideoPattern) || []
|
|
fileVideoUrls.forEach(u => { if (!urls.includes(u)) urls.push(u) })
|
|
|
|
const allUrlPattern = /https?:\/\/[^\s<>"']+(?=\s|>|"|'|$)/gi
|
|
const allUrls: string[] = html.match(allUrlPattern) || []
|
|
allUrls.forEach(u => {
|
|
const classification = classifyUrl(u)
|
|
if (classification.type === 'video' && !urls.includes(u)) {
|
|
urls.push(u)
|
|
}
|
|
})
|
|
|
|
return urls
|
|
}, [html, renderVideoLinksAsEmbeds])
|
|
|
|
// If no video embedding is enabled, just render the HTML normally
|
|
if (!renderVideoLinksAsEmbeds || videoUrls.length === 0) {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={className}
|
|
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
|
onMouseUp={onMouseUp}
|
|
onTouchEnd={onTouchEnd}
|
|
/>
|
|
)
|
|
}
|
|
|
|
// Split the HTML by video placeholders and render with embedded players
|
|
const parts = processedHtml.split(/(__VIDEO_EMBED_\d+__)/)
|
|
|
|
return (
|
|
<div ref={ref} className={className} onMouseUp={onMouseUp} onTouchEnd={onTouchEnd}>
|
|
{parts.map((part, index) => {
|
|
const videoMatch = part.match(/^__VIDEO_EMBED_(\d+)__$/)
|
|
if (videoMatch) {
|
|
const videoIndex = parseInt(videoMatch[1])
|
|
const videoUrl = videoUrls[videoIndex]
|
|
if (videoUrl) {
|
|
return (
|
|
<div key={index} className="reader-video" style={{ margin: '1rem 0' }}>
|
|
<ReactPlayer
|
|
url={videoUrl}
|
|
controls
|
|
width="100%"
|
|
height="auto"
|
|
style={{
|
|
width: '100%',
|
|
height: 'auto',
|
|
aspectRatio: '16/9'
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
// Regular HTML content
|
|
return (
|
|
<div
|
|
key={index}
|
|
dangerouslySetInnerHTML={{ __html: part }}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
})
|
|
|
|
VideoEmbedProcessor.displayName = 'VideoEmbedProcessor'
|
|
|
|
export default VideoEmbedProcessor
|