From 213c299603cb108df27bd371be1a12e66b2a95a8 Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 12 Dec 2025 18:45:39 +0100 Subject: [PATCH] Updated previews. --- css/components/rant.css | 5 +++ js/components/comment-item.js | 7 +++ js/components/rant-card.js | 7 +++ js/components/rant-content.js | 81 +++++++++++++++++++++++++++-------- js/components/rant-detail.js | 7 +++ js/utils/url.js | 61 +++++++++++++++++++++++++- 6 files changed, 150 insertions(+), 18 deletions(-) diff --git a/css/components/rant.css b/css/components/rant.css index a510d26..50d21d0 100644 --- a/css/components/rant.css +++ b/css/components/rant.css @@ -24,6 +24,11 @@ rant-content { .content-text a { color: var(--color-link); + text-decoration: none; +} + +.content-text a:hover { + text-decoration: underline; } .content-text code { diff --git a/js/components/comment-item.js b/js/components/comment-item.js index a2a04f4..b2c1d12 100644 --- a/js/components/comment-item.js +++ b/js/components/comment-item.js @@ -91,6 +91,13 @@ class CommentItem extends BaseComponent { `); + + if (comment.links && comment.links.length > 0) { + const rantContent = this.$('rant-content'); + if (rantContent) { + rantContent.setLinks(comment.links); + } + } } escapeAttr(str) { diff --git a/js/components/rant-card.js b/js/components/rant-card.js index eef6808..0a500a3 100644 --- a/js/components/rant-card.js +++ b/js/components/rant-card.js @@ -87,6 +87,13 @@ class RantCard extends BaseComponent { `); + + if (rant.links && rant.links.length > 0) { + const rantContent = this.$('rant-content'); + if (rantContent) { + rantContent.setLinks(rant.links); + } + } } escapeAttr(str) { diff --git a/js/components/rant-content.js b/js/components/rant-content.js index 986922b..6b933e8 100644 --- a/js/components/rant-content.js +++ b/js/components/rant-content.js @@ -6,37 +6,59 @@ */ import { BaseComponent } from './base-component.js'; -import { markdownRenderer } from '../utils/markdown.js'; -import { extractImageUrls, extractYoutubeUrls, extractNonMediaUrls } from '../utils/url.js'; +import { categorizeLinks, processTextWithLinks, extractImageUrls, extractYoutubeUrls, extractNonMediaUrls, sanitizeUrl } from '../utils/url.js'; class RantContent extends BaseComponent { static get observedAttributes() { - return ['text']; + return ['text', 'links']; } init() { + this.linksData = null; + this.render(); + } + + setLinks(links) { + this.linksData = links; this.render(); } render() { const text = this.getAttr('text') || ''; + const linksAttr = this.getAttr('links'); + const links = this.linksData || (linksAttr ? JSON.parse(linksAttr) : null); this.addClass('rant-content'); - const images = extractImageUrls(text); - const youtubeLinks = extractYoutubeUrls(text); - const otherLinks = extractNonMediaUrls(text); + let images = []; + let youtubeLinks = []; + let otherLinks = []; + let renderedText = ''; - const renderedText = markdownRenderer.render(text); + if (links && links.length > 0) { + const categorized = categorizeLinks(links); + images = categorized.images.map(l => l.url); + youtubeLinks = categorized.youtube.map(l => l.url); + otherLinks = categorized.other; + renderedText = processTextWithLinks(text, links); + renderedText = renderedText.replace(/\n/g, '
'); + } else { + images = extractImageUrls(text); + youtubeLinks = extractYoutubeUrls(text); + const extractedOther = extractNonMediaUrls(text); + otherLinks = extractedOther.map(url => ({ url, title: url })); + renderedText = this.escapeHtml(text).replace(/\n/g, '
'); + } let html = `
${renderedText}
`; if (images.length > 0) { html += `
- ${images.map(url => ` - - `).join('')} + ${images.map(url => { + const safeUrl = sanitizeUrl(url); + return safeUrl ? `` : ''; + }).join('')}
`; } @@ -44,9 +66,10 @@ class RantContent extends BaseComponent { if (youtubeLinks.length > 0) { html += `
- ${youtubeLinks.map(url => ` - - `).join('')} + ${youtubeLinks.map(url => { + const safeUrl = sanitizeUrl(url); + return safeUrl ? `` : ''; + }).join('')}
`; } @@ -54,9 +77,12 @@ class RantContent extends BaseComponent { if (otherLinks.length > 0) { html += ` `; } @@ -64,11 +90,32 @@ class RantContent extends BaseComponent { this.setHtml(html); } + escapeHtml(str) { + if (!str) return ''; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + escapeAttr(str) { + if (!str) return ''; + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + onAttributeChanged(name, oldValue, newValue) { this.render(); } - setText(text) { + setText(text, links = null) { + this.linksData = links; this.setAttr('text', text); } } diff --git a/js/components/rant-detail.js b/js/components/rant-detail.js index 63cb17c..882cc4a 100644 --- a/js/components/rant-detail.js +++ b/js/components/rant-detail.js @@ -183,6 +183,13 @@ class RantDetail extends BaseComponent { `); this.initComments(); + + if (rant.links && rant.links.length > 0) { + const rantContent = this.$('rant-content'); + if (rantContent) { + rantContent.setLinks(rant.links); + } + } } escapeAttr(str) { diff --git a/js/utils/url.js b/js/utils/url.js index 8d6efcb..67fe289 100644 --- a/js/utils/url.js +++ b/js/utils/url.js @@ -100,6 +100,62 @@ function buildAvatarUrl(avatar) { 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 + `${escapedText}` + after; + } + } + const parts = result.split(/(]*>.*?<\/a>)/g); + return parts.map(part => { + if (part.startsWith('/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + export { extractUrls, isYoutubeUrl, @@ -116,5 +172,8 @@ export { makeAbsoluteUrl, isDevrantImageUrl, buildDevrantImageUrl, - buildAvatarUrl + buildAvatarUrl, + categorizeLinks, + processTextWithLinks, + escapeHtml };