processedLine = processedLine.replace(rawVideoRegex, (match, url) => {
let fullUrl = url;
if (!url.startsWith('http') && !url.startsWith('//') && !url.startsWith('/')) fullUrl = '//' + url;
return `[video](${fullUrl})`;
});
processedLine = processedLine.replace(rawAudioRegex, (match, url) => {
let fullUrl = url;
if (!url.startsWith('http') && !url.startsWith('//') && !url.startsWith('/')) fullUrl = '//' + url;
return `[audio](${fullUrl})`;
});
// 3. Render Markdown for the line
let mdSafe = processedLine.replace(/\*/g, '\\*').replace(/_/g, '\\_');
const bs = String.fromCharCode(92);
mdSafe = mdSafe.split(bs + bs + '_').join(bs + bs + bs + '_');
let rendered = marked.parseInline
? marked.parseInline(mdSafe, { renderer: renderer })
: marked.parse(mdSafe, { renderer: renderer }).replace(/|<\/p>/g, '');
// 4. Emojis
rendered = rendered.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n));
return rendered;
});
let md = renderedLines.join('\n');
// YouTube embed: replace anchor links pointing to YouTube with an embedded player
// Respects per-user preference (session) when logged in; falls back to global config flag for guests.
const embedYoutube = window.f0ckSession
? window.f0ckSession.embed_youtube_in_comments !== false
: window.f0ckEmbedYoutubeInComments !== false;
if (embedYoutube) {
md = md.replace(
/]*href="https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?(?:[^"]*&(?:amp;)?)?v=|youtu\.be\/)([a-zA-Z0-9_\-]{11})[^"]*"[^>]*>([\s\S]*?)<\/a>/gi,
(match, videoId) => {
return ``;
}
);
}
// Vocaroo embed
md = md.replace(
/]*href="https?:\/\/(?:www\.)?(?:voca\.ro|vocaroo\.com)\/([a-zA-Z0-9_-]+)[^"]*"[^>]*>([\s\S]*?)<\/a>/gi,
(match, vocarooId) => {
if (['upload', 'contact', 'privacy', 'tos', 'about'].includes(vocarooId.toLowerCase())) return match;
return ``;
}
);
// Abyss label replacement
md = md.replace(
/]*href="(?:https?:\/\/[^\/]+)?\/abyss(?:#|\/)(\d+)"[^>]*>([\s\S]*?)<\/a>/gi,
(match, abyssId) => {
return ` /abyss/${abyssId}`;
}
);
const mediaHosts = [escapedSiteHost];
if (window.f0ckAllowedImages && Array.isArray(window.f0ckAllowedImages)) {
window.f0ckAllowedImages.forEach(h => {
const escaped = h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
mediaHosts.push(`(?:[a-z0-9-]+\\.)*${escaped}`);
});
}
const mediaHostsPart = mediaHosts.join('|');
const mediaDomainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${mediaHostsPart})|(?=\\/[a-zA-Z0-9_\\-]))`;
// Video embed: replace anchor links pointing to video files from allowed hosters with a video player
const videoEmbedRegex = new RegExp(`]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
md = md.replace(videoEmbedRegex, (match, url) => {
const isConvertedGif = url.endsWith('#gif');
const cleanUrl = url.replace(/#gif$/, '');
let deleteBtn = '';
if (this.isAdmin && cleanUrl.startsWith('/c/')) {
const filename = cleanUrl.substring(3);
deleteBtn = ``;
}
if (isConvertedGif) {
return `${deleteBtn}`;
}
return `${deleteBtn}`;
});
// Audio embed: replace anchor links pointing to audio files from allowed hosters with an audio player
const audioEmbedRegex = new RegExp(`]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp3|ogg|wav|flac|aac|opus|m4a)(?:\\?[^\\s\\[\\]\\(\\)]+)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
md = md.replace(audioEmbedRegex, (match, url) => {
let deleteBtn = '';
if (this.isAdmin && url.startsWith('/c/')) {
const filename = url.substring(3);
deleteBtn = ``;
}
return `${deleteBtn}`;
});
// Handle spoilers [spoiler]text[/spoiler] (supports nesting)
let prevMd;
let iterations = 0;
const spoilerRegex = /\[spoiler\]((?:(?!\[spoiler\])[\s\S])*?)\[\/spoiler\]/gi;
do {
prevMd = md;
md = md.replace(spoilerRegex, (match, content) => {
return `${content}`;
});
iterations++;
} while (md !== prevMd && iterations < 10);
// Handle blur [blur]text[/blur] (supports nesting)
const blurRegex = /\[blur\]((?:(?!\[blur\])[\s\S])*?)\[\/blur\]/gi;
iterations = 0;
do {
prevMd = md;
md = md.replace(blurRegex, (match, content) => {
return `${content}`;
});
iterations++;
} while (md !== prevMd && iterations < 10);
// Restore protected code blocks
md = md.replace(/BLOCKPORTALX(\d+)X/g, (match, index) => {
return codeBlocks[index] || '';
});
if (window.Sanitizer && typeof Sanitizer.clean === 'function') {
md = Sanitizer.clean(md);
}
// Append the "show full comment" button AFTER sanitization — the sanitizer
// whitelist strips