/**
* @fileoverview URL Utilities for Rantii
* @author retoor <retoor@molodetz.nl>
* @description URL parsing and detection utilities
* @keywords url, link, youtube, image, detection
*/
const URL_REGEX = /https?:\/\/[^\s<]+[^<.,:;"')\]\s]/gi;
const YOUTUBE_REGEX = /(?:youtube\.com\/(?:watch\?v=|embed\/|v\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i;
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'];
function extractUrls(text) {
const matches = text.match(URL_REGEX);
return matches || [];
}
function isYoutubeUrl(url) {
return YOUTUBE_REGEX.test(url);
}
function getYoutubeVideoId(url) {
const match = url.match(YOUTUBE_REGEX);
return match ? match[1] : null;
}
function getYoutubeThumbnail(videoId) {
return `/api/proxy-image?url=${encodeURIComponent(`https://img.youtube.com/vi/${videoId}/hqdefault.jpg`)}`;
}
function getYoutubeEmbedUrl(videoId) {
return `https://www.youtube-nocookie.com/embed/${videoId}`;
}
function isImageUrl(url) {
const lower = url.toLowerCase();
return IMAGE_EXTENSIONS.some(ext => lower.includes(ext));
}
function isGifUrl(url) {
return url.toLowerCase().includes('.gif');
}
function extractImageUrls(text) {
return extractUrls(text).filter(isImageUrl);
}
function extractYoutubeUrls(text) {
return extractUrls(text).filter(isYoutubeUrl);
}
function extractNonMediaUrls(text) {
return extractUrls(text).filter(url => !isImageUrl(url) && !isYoutubeUrl(url));
}
function sanitizeUrl(url) {
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null;
}
return parsed.href;
} catch {
return null;
}
}
function getDomain(url) {
try {
const parsed = new URL(url);
return parsed.hostname;
} catch {
return null;
}
}
function makeAbsoluteUrl(url, base) {
try {
return new URL(url, base).href;
} catch {
return url;
}
}
function isDevrantImageUrl(url) {
return url.includes('devrant.com') || url.includes('devrant.io');
}
function buildDevrantImageUrl(imagePath) {
if (!imagePath) return null;
if (imagePath.startsWith('http')) {
return `/api/proxy-image?url=${encodeURIComponent(imagePath)}`;
}
return `/api/proxy-image?url=${encodeURIComponent(`https://img.devrant.com/${imagePath}`)}`;
}
function buildAvatarUrl(avatar) {
if (!avatar || !avatar.i) {
return null;
}
return `/api/proxy-image?url=${encodeURIComponent(`https://avatars.devrant.com/${avatar.i}`)}`;
}
function categorizeLinks(links) {
if (!links || !Array.isArray(links)) {
return { images: [], youtube: [], other: [] };
}
const images = [];
const youtube = [];
const other = [];
for (const link of links) {
if (!link.url) continue;
if (isImageUrl(link.url)) {
images.push(link);
} else if (isYoutubeUrl(link.url)) {
youtube.push(link);
} else {
other.push(link);
}
}
return { images, youtube, other };
}
function processTextWithLinks(text, links) {
if (!text) return '';
if (!links || !Array.isArray(links) || links.length === 0) {
return escapeHtml(text);
}
const sortedLinks = [...links].sort((a, b) => b.start - a.start);
let result = text;
for (const link of sortedLinks) {
if (typeof link.start !== 'number' || typeof link.end !== 'number') continue;
if (link.start < 0 || link.end > result.length) continue;
const before = result.substring(0, link.start);
const linkText = result.substring(link.start, link.end);
const after = result.substring(link.end);
const safeUrl = sanitizeUrl(link.url);
if (safeUrl) {
const escapedText = escapeHtml(linkText);
result = before + `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer" class="inline-link">${escapedText}</a>` + after;
}
}
const parts = result.split(/(<a[^>]*>.*?<\/a>)/g);
return parts.map(part => {
if (part.startsWith('<a ')) return part;
return escapeHtml(part);
}).join('');
}
function escapeHtml(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
export {
extractUrls,
isYoutubeUrl,
getYoutubeVideoId,
getYoutubeThumbnail,
getYoutubeEmbedUrl,
isImageUrl,
isGifUrl,
extractImageUrls,
extractYoutubeUrls,
extractNonMediaUrls,
sanitizeUrl,
getDomain,
makeAbsoluteUrl,
isDevrantImageUrl,
buildDevrantImageUrl,
buildAvatarUrl,
categorizeLinks,
processTextWithLinks,
escapeHtml
};