Updated previews.
Some checks failed
CI / test (3.11) (push) Failing after 24s
CI / test (3.10) (push) Failing after 13s
CI / test (3.12) (push) Failing after 25s
CI / lint (push) Failing after 25s
CI / test (3.9) (push) Failing after 25s
CI / build (push) Failing after 26s
CI / test (3.8) (push) Failing after 35s

This commit is contained in:
retoor 2025-12-12 18:45:39 +01:00
parent 3570ba5b99
commit 213c299603
6 changed files with 150 additions and 18 deletions

View File

@ -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 {

View File

@ -91,6 +91,13 @@ class CommentItem extends BaseComponent {
</footer>
</article>
`);
if (comment.links && comment.links.length > 0) {
const rantContent = this.$('rant-content');
if (rantContent) {
rantContent.setLinks(comment.links);
}
}
}
escapeAttr(str) {

View File

@ -87,6 +87,13 @@ class RantCard extends BaseComponent {
</footer>
</article>
`);
if (rant.links && rant.links.length > 0) {
const rantContent = this.$('rant-content');
if (rantContent) {
rantContent.setLinks(rant.links);
}
}
}
escapeAttr(str) {

View File

@ -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, '<br>');
} 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, '<br>');
}
let html = `<div class="content-text">${renderedText}</div>`;
if (images.length > 0) {
html += `
<div class="content-images">
${images.map(url => `
<image-preview src="${url}"></image-preview>
`).join('')}
${images.map(url => {
const safeUrl = sanitizeUrl(url);
return safeUrl ? `<image-preview src="${safeUrl}"></image-preview>` : '';
}).join('')}
</div>
`;
}
@ -44,9 +66,10 @@ class RantContent extends BaseComponent {
if (youtubeLinks.length > 0) {
html += `
<div class="content-videos">
${youtubeLinks.map(url => `
<youtube-embed url="${url}"></youtube-embed>
`).join('')}
${youtubeLinks.map(url => {
const safeUrl = sanitizeUrl(url);
return safeUrl ? `<youtube-embed url="${safeUrl}"></youtube-embed>` : '';
}).join('')}
</div>
`;
}
@ -54,9 +77,12 @@ class RantContent extends BaseComponent {
if (otherLinks.length > 0) {
html += `
<div class="content-links">
${otherLinks.slice(0, 3).map(url => `
<link-preview url="${url}"></link-preview>
`).join('')}
${otherLinks.slice(0, 3).map(link => {
const url = typeof link === 'string' ? link : link.url;
const title = typeof link === 'string' ? link : (link.title || link.url);
const safeUrl = sanitizeUrl(url);
return safeUrl ? `<link-preview url="${safeUrl}" title="${this.escapeAttr(title)}"></link-preview>` : '';
}).join('')}
</div>
`;
}
@ -64,11 +90,32 @@ class RantContent extends BaseComponent {
this.setHtml(html);
}
escapeHtml(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
escapeAttr(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
onAttributeChanged(name, oldValue, newValue) {
this.render();
}
setText(text) {
setText(text, links = null) {
this.linksData = links;
this.setAttr('text', text);
}
}

View File

@ -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) {

View File

@ -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 + `<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,
@ -116,5 +172,8 @@ export {
makeAbsoluteUrl,
isDevrantImageUrl,
buildDevrantImageUrl,
buildAvatarUrl
buildAvatarUrl,
categorizeLinks,
processTextWithLinks,
escapeHtml
};