Browse Source

asciidoc youtube rendering

imwald
Silberengel 4 months ago
parent
commit
ae53b52239
  1. 120
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  2. 19
      src/components/Note/MarkdownArticle/preprocessMarkup.ts

120
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -2,6 +2,7 @@ import { useSecondaryPage, useSmartHashtagNavigation, useSmartRelayNavigation }
import Image from '@/components/Image' import Image from '@/components/Image'
import MediaPlayer from '@/components/MediaPlayer' import MediaPlayer from '@/components/MediaPlayer'
import WebPreview from '@/components/WebPreview' import WebPreview from '@/components/WebPreview'
import YoutubeEmbeddedPlayer from '@/components/YoutubeEmbeddedPlayer'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNoteList } from '@/lib/link' import { toNoteList } from '@/lib/link'
import { useMediaExtraction } from '@/hooks' import { useMediaExtraction } from '@/hooks'
@ -17,7 +18,7 @@ import { EmbeddedNote, EmbeddedMention } from '@/components/Embedded'
import Wikilink from '@/components/UniversalContent/Wikilink' import Wikilink from '@/components/UniversalContent/Wikilink'
import { preprocessAsciidocMediaLinks } from '../MarkdownArticle/preprocessMarkup' import { preprocessAsciidocMediaLinks } from '../MarkdownArticle/preprocessMarkup'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { WS_URL_REGEX } from '@/constants' import { WS_URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants'
/** /**
* Truncate link display text to 200 characters, adding ellipsis if truncated * Truncate link display text to 200 characters, adding ellipsis if truncated
@ -29,6 +30,17 @@ function truncateLinkText(text: string, maxLength: number = 200): string {
return text.substring(0, maxLength) + '...' return text.substring(0, maxLength) + '...'
} }
/**
* Check if a URL is a YouTube URL
*/
function isYouTubeUrl(url: string): boolean {
// Create a new regex instance to avoid state issues with global regex
// Keep the 'i' flag for case-insensitivity but remove 'g' to avoid state issues
const flags = YOUTUBE_URL_REGEX.flags.replace('g', '')
const regex = new RegExp(YOUTUBE_URL_REGEX.source, flags)
return regex.test(url)
}
export default function AsciidocArticle({ export default function AsciidocArticle({
event, event,
className, className,
@ -112,7 +124,29 @@ export default function AsciidocArticle({
return media return media
}, [event.id, JSON.stringify(event.tags)]) }, [event.id, JSON.stringify(event.tags)])
// Extract non-media links from tags // Extract YouTube URLs from tags (for display at top)
const tagYouTubeUrls = useMemo(() => {
const youtubeUrls: string[] = []
const seenUrls = new Set<string>()
event.tags
.filter(tag => tag[0] === 'r' && tag[1])
.forEach(tag => {
const url = tag[1]
if (!url.startsWith('http://') && !url.startsWith('https://')) return
if (!isYouTubeUrl(url)) return
const cleaned = cleanUrl(url)
if (cleaned && !seenUrls.has(cleaned)) {
youtubeUrls.push(cleaned)
seenUrls.add(cleaned)
}
})
return youtubeUrls
}, [event.id, JSON.stringify(event.tags)])
// Extract non-media links from tags (excluding YouTube URLs)
const tagLinks = useMemo(() => { const tagLinks = useMemo(() => {
const links: string[] = [] const links: string[] = []
const seenUrls = new Set<string>() const seenUrls = new Set<string>()
@ -123,6 +157,7 @@ export default function AsciidocArticle({
const url = tag[1] const url = tag[1]
if (!url.startsWith('http://') && !url.startsWith('https://')) return if (!url.startsWith('http://') && !url.startsWith('https://')) return
if (isImage(url) || isMedia(url)) return if (isImage(url) || isMedia(url)) return
if (isYouTubeUrl(url)) return // Exclude YouTube URLs
const cleaned = cleanUrl(url) const cleaned = cleanUrl(url)
if (cleaned && !seenUrls.has(cleaned)) { if (cleaned && !seenUrls.has(cleaned)) {
@ -185,7 +220,22 @@ export default function AsciidocArticle({
return urls return urls
}, [event.content]) }, [event.content])
// Extract non-media links from content // Extract YouTube URLs from content
const youtubeUrlsInContent = useMemo(() => {
const urls = new Set<string>()
const urlRegex = /https?:\/\/[^\s<>"']+/g
let match
while ((match = urlRegex.exec(event.content)) !== null) {
const url = match[0]
const cleaned = cleanUrl(url)
if (cleaned && isYouTubeUrl(cleaned)) {
urls.add(cleaned)
}
}
return urls
}, [event.content])
// Extract non-media links from content (excluding YouTube URLs)
const contentLinks = useMemo(() => { const contentLinks = useMemo(() => {
const links: string[] = [] const links: string[] = []
const seenUrls = new Set<string>() const seenUrls = new Set<string>()
@ -193,7 +243,7 @@ export default function AsciidocArticle({
let match let match
while ((match = urlRegex.exec(event.content)) !== null) { while ((match = urlRegex.exec(event.content)) !== null) {
const url = match[0] const url = match[0]
if ((url.startsWith('http://') || url.startsWith('https://')) && !isImage(url) && !isMedia(url)) { if ((url.startsWith('http://') || url.startsWith('https://')) && !isImage(url) && !isMedia(url) && !isYouTubeUrl(url)) {
const cleaned = cleanUrl(url) const cleaned = cleanUrl(url)
if (cleaned && !seenUrls.has(cleaned)) { if (cleaned && !seenUrls.has(cleaned)) {
links.push(cleaned) links.push(cleaned)
@ -225,6 +275,14 @@ export default function AsciidocArticle({
}) })
}, [tagMedia, mediaUrlsInContent, metadata.image, hideImagesAndInfo]) }, [tagMedia, mediaUrlsInContent, metadata.image, hideImagesAndInfo])
// Filter tag YouTube URLs to only show what's not in content
const leftoverTagYouTubeUrls = useMemo(() => {
return tagYouTubeUrls.filter(url => {
const cleaned = cleanUrl(url)
return cleaned && !youtubeUrlsInContent.has(cleaned)
})
}, [tagYouTubeUrls, youtubeUrlsInContent])
// Filter tag links to only show what's not in content (to avoid duplicate WebPreview cards) // Filter tag links to only show what's not in content (to avoid duplicate WebPreview cards)
const leftoverTagLinks = useMemo(() => { const leftoverTagLinks = useMemo(() => {
const contentLinksSet = new Set(contentLinks.map(link => cleanUrl(link)).filter(Boolean)) const contentLinksSet = new Set(contentLinks.map(link => cleanUrl(link)).filter(Boolean))
@ -331,8 +389,13 @@ export default function AsciidocArticle({
return `<span data-wikilink="${escaped}" class="wikilink-placeholder"></span>` return `<span data-wikilink="${escaped}" class="wikilink-placeholder"></span>`
}) })
// Handle relay URLs (wss:// or ws://) in links - convert to relay page links // Handle YouTube URLs and relay URLs in links
htmlString = htmlString.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/g, (match, href, linkText) => { htmlString = htmlString.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/g, (match, href, linkText) => {
// Check if the href is a YouTube URL
if (isYouTubeUrl(href)) {
const cleanedUrl = cleanUrl(href)
return `<div data-youtube-url="${cleanedUrl.replace(/"/g, '&quot;')}" class="youtube-placeholder my-2"></div>`
}
// Check if the href is a relay URL // Check if the href is a relay URL
if (isWebsocketUrl(href)) { if (isWebsocketUrl(href)) {
const relayPath = `/relays/${encodeURIComponent(href)}` const relayPath = `/relays/${encodeURIComponent(href)}`
@ -343,6 +406,18 @@ export default function AsciidocArticle({
return match.replace(/<a/, `<a data-original-text="${escapedLinkText}"`) return match.replace(/<a/, `<a data-original-text="${escapedLinkText}"`)
}) })
// Handle YouTube URLs in plain text (not in <a> tags)
// Create a new regex instance to avoid state issues
const youtubeRegex = new RegExp(YOUTUBE_URL_REGEX.source, YOUTUBE_URL_REGEX.flags)
htmlString = htmlString.replace(youtubeRegex, (match) => {
// Only replace if not already in a tag (basic check)
if (!match.includes('<') && !match.includes('>') && isYouTubeUrl(match)) {
const cleanedUrl = cleanUrl(match)
return `<div data-youtube-url="${cleanedUrl.replace(/"/g, '&quot;')}" class="youtube-placeholder my-2"></div>`
}
return match
})
// Handle relay URLs in plain text (not in <a> tags) - convert to relay page links // Handle relay URLs in plain text (not in <a> tags) - convert to relay page links
htmlString = htmlString.replace(WS_URL_REGEX, (match) => { htmlString = htmlString.replace(WS_URL_REGEX, (match) => {
// Only replace if not already in a tag (basic check) // Only replace if not already in a tag (basic check)
@ -418,6 +493,23 @@ export default function AsciidocArticle({
reactRootsRef.current.set(container, root) reactRootsRef.current.set(container, root)
}) })
// Process YouTube URLs - replace placeholders with React components
const youtubePlaceholders = contentRef.current.querySelectorAll('.youtube-placeholder[data-youtube-url]')
youtubePlaceholders.forEach((element) => {
const youtubeUrl = element.getAttribute('data-youtube-url')
if (!youtubeUrl) return
// Create a container for React component
const container = document.createElement('div')
container.className = 'my-2'
element.parentNode?.replaceChild(container, element)
// Use React to render the component
const root = createRoot(container)
root.render(<YoutubeEmbeddedPlayer url={youtubeUrl} className="max-w-[400px]" mustLoad={false} />)
reactRootsRef.current.set(container, root)
})
// Process wikilinks - replace placeholders with React components // Process wikilinks - replace placeholders with React components
const wikilinks = contentRef.current.querySelectorAll('.wikilink-placeholder[data-wikilink]') const wikilinks = contentRef.current.querySelectorAll('.wikilink-placeholder[data-wikilink]')
wikilinks.forEach((element) => { wikilinks.forEach((element) => {
@ -753,6 +845,24 @@ export default function AsciidocArticle({
</div> </div>
)} )}
{/* YouTube URLs from tags (only if not in content) */}
{leftoverTagYouTubeUrls.length > 0 && (
<div className="space-y-4 mb-6">
{leftoverTagYouTubeUrls.map((url) => {
const cleaned = cleanUrl(url)
return (
<div key={`tag-youtube-${cleaned}`} className="my-2">
<YoutubeEmbeddedPlayer
url={url}
className="max-w-[400px]"
mustLoad={false}
/>
</div>
)
})}
</div>
)}
{/* Parsed AsciiDoc content */} {/* Parsed AsciiDoc content */}
{isLoading ? ( {isLoading ? (
<div>Loading content...</div> <div>Loading content...</div>

19
src/components/Note/MarkdownArticle/preprocessMarkup.ts

@ -1,5 +1,15 @@
import { isImage, isVideo, isAudio } from '@/lib/url' import { isImage, isVideo, isAudio } from '@/lib/url'
import { URL_REGEX } from '@/constants' import { URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants'
/**
* Check if a URL is a YouTube URL
*/
function isYouTubeUrl(url: string): boolean {
// Create a new regex instance to avoid state issues with global regex
const flags = YOUTUBE_URL_REGEX.flags.replace('g', '')
const regex = new RegExp(YOUTUBE_URL_REGEX.source, flags)
return regex.test(url)
}
/** /**
* Preprocess content to convert raw media URLs and hyperlinks to markdown syntax * Preprocess content to convert raw media URLs and hyperlinks to markdown syntax
@ -151,10 +161,11 @@ export function preprocessAsciidocMediaLinks(content: string): string {
continue // In code block continue // In code block
} }
// Check if it's a media URL // Check if it's a media URL or YouTube URL
const isImageUrl = isImage(url) const isImageUrl = isImage(url)
const isVideoUrl = isVideo(url) const isVideoUrl = isVideo(url)
const isAudioUrl = isAudio(url) const isAudioUrl = isAudio(url)
const isYouTube = isYouTubeUrl(url)
let replacement: string let replacement: string
if (isImageUrl) { if (isImageUrl) {
@ -166,6 +177,10 @@ export function preprocessAsciidocMediaLinks(content: string): string {
} else if (isAudioUrl) { } else if (isAudioUrl) {
// Audio: convert to audio::url[] // Audio: convert to audio::url[]
replacement = `audio::${url}[]` replacement = `audio::${url}[]`
} else if (isYouTube) {
// YouTube URLs: convert to link:url[url] (will be handled in post-processing)
// This allows AsciiDoc to process it as a link, then we'll replace it with YouTube player
replacement = `link:${url}[${url}]`
} else { } else {
// Regular hyperlinks: convert to link:url[url] // Regular hyperlinks: convert to link:url[url]
replacement = `link:${url}[${url}]` replacement = `link:${url}[${url}]`

Loading…
Cancel
Save