|
/**
|
|
* @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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
export {
|
|
extractUrls,
|
|
isYoutubeUrl,
|
|
getYoutubeVideoId,
|
|
getYoutubeThumbnail,
|
|
getYoutubeEmbedUrl,
|
|
isImageUrl,
|
|
isGifUrl,
|
|
extractImageUrls,
|
|
extractYoutubeUrls,
|
|
extractNonMediaUrls,
|
|
sanitizeUrl,
|
|
getDomain,
|
|
makeAbsoluteUrl,
|
|
isDevrantImageUrl,
|
|
buildDevrantImageUrl,
|
|
buildAvatarUrl,
|
|
categorizeLinks,
|
|
processTextWithLinks,
|
|
escapeHtml
|
|
};
|