misc
This commit is contained in:
@@ -443,226 +443,226 @@ function parseDiscordMarkdown(text, options = {}) {
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// Code blocks (``` ```) - add data-lenis-prevent for independent scrolling
|
||||
// Must be processed first to prevent other formatting inside code
|
||||
// Don't trim - preserve whitespace for ASCII art
|
||||
parsed = parsed.replace(/```(\w+)?\n?([\s\S]*?)```/g, (_, lang, code) => {
|
||||
// Only trim trailing newline, preserve all other whitespace
|
||||
const trimmedCode = code.replace(/\n$/, '');
|
||||
return `<pre class="discord-code-block" data-lenis-prevent><code>${trimmedCode}</code></pre>`;
|
||||
});
|
||||
// Code blocks (``` ```) - add data-lenis-prevent for independent scrolling
|
||||
// Must be processed first to prevent other formatting inside code
|
||||
// Don't trim - preserve whitespace for ASCII art
|
||||
parsed = parsed.replace(/```(\w+)?\n?([\s\S]*?)```/g, (_, lang, code) => {
|
||||
// Only trim trailing newline, preserve all other whitespace
|
||||
const trimmedCode = code.replace(/\n$/, '');
|
||||
return `<pre class="discord-code-block" data-lenis-prevent><code>${trimmedCode}</code></pre>`;
|
||||
});
|
||||
|
||||
// Inline code (`) - must be early to prevent formatting inside code
|
||||
parsed = parsed.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
// Inline code (`) - must be early to prevent formatting inside code
|
||||
parsed = parsed.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
|
||||
// Blockquotes (> at start of line) - process before newline conversion
|
||||
// Group consecutive > lines into a single blockquote
|
||||
parsed = parsed.replace(/(^|\n)((?:> .+(?:\n|$))+)/gm, (_, before, block) => {
|
||||
const content = block
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^> /, ''))
|
||||
.join('\n');
|
||||
return `${before}<blockquote class="discord-blockquote" data-lenis-prevent>${content}</blockquote>`;
|
||||
});
|
||||
// Blockquotes (> at start of line) - process before newline conversion
|
||||
// Group consecutive > lines into a single blockquote
|
||||
parsed = parsed.replace(/(^|\n)((?:> .+(?:\n|$))+)/gm, (_, before, block) => {
|
||||
const content = block
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^> /, ''))
|
||||
.join('\n');
|
||||
return `${before}<blockquote class="discord-blockquote" data-lenis-prevent>${content}</blockquote>`;
|
||||
});
|
||||
|
||||
// Headings (# ## ###) - must be at start of line
|
||||
// Process before other inline formatting
|
||||
parsed = parsed.replace(/(^|\n)### (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-heading discord-heading-3">${content}</span>`;
|
||||
});
|
||||
parsed = parsed.replace(/(^|\n)## (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-heading discord-heading-2">${content}</span>`;
|
||||
});
|
||||
parsed = parsed.replace(/(^|\n)# (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-heading discord-heading-1">${content}</span>`;
|
||||
});
|
||||
// Headings (# ## ###) - must be at start of line
|
||||
// Process before other inline formatting
|
||||
parsed = parsed.replace(/(^|\n)### (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-heading discord-heading-3">${content}</span>`;
|
||||
});
|
||||
parsed = parsed.replace(/(^|\n)## (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-heading discord-heading-2">${content}</span>`;
|
||||
});
|
||||
parsed = parsed.replace(/(^|\n)# (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-heading discord-heading-1">${content}</span>`;
|
||||
});
|
||||
|
||||
// Subtext/small text (-# at start of line) - process before newline conversion
|
||||
parsed = parsed.replace(/(^|\n)-# (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-subtext">${content}</span>`;
|
||||
});
|
||||
// Subtext/small text (-# at start of line) - process before newline conversion
|
||||
parsed = parsed.replace(/(^|\n)-# (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-subtext">${content}</span>`;
|
||||
});
|
||||
|
||||
// Unordered lists (- or * at start of line, but not ---)
|
||||
parsed = parsed.replace(/(^|\n)[-*] (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-list-item">• ${content}</span>`;
|
||||
});
|
||||
// Unordered lists (- or * at start of line, but not ---)
|
||||
parsed = parsed.replace(/(^|\n)[-*] (.+?)(?=\n|$)/gm, (_, before, content) => {
|
||||
return `${before}<span class="discord-list-item">• ${content}</span>`;
|
||||
});
|
||||
|
||||
// Ordered lists (1. 2. etc at start of line)
|
||||
parsed = parsed.replace(/(^|\n)(\d+)\. (.+?)(?=\n|$)/gm, (_, before, num, content) => {
|
||||
return `${before}<span class="discord-list-item">${num}. ${content}</span>`;
|
||||
});
|
||||
// Ordered lists (1. 2. etc at start of line)
|
||||
parsed = parsed.replace(/(^|\n)(\d+)\. (.+?)(?=\n|$)/gm, (_, before, num, content) => {
|
||||
return `${before}<span class="discord-list-item">${num}. ${content}</span>`;
|
||||
});
|
||||
|
||||
// Bold + Italic + Underline combinations (most specific first)
|
||||
// ___***text***___ or ***___text___***
|
||||
parsed = parsed.replace(/(\*\*\*|___)(\*\*\*|___)([^*_]+)\2\1/g, '<strong><em><u>$3</u></em></strong>');
|
||||
// Bold + Italic + Underline combinations (most specific first)
|
||||
// ___***text***___ or ***___text___***
|
||||
parsed = parsed.replace(/(\*\*\*|___)(\*\*\*|___)([^*_]+)\2\1/g, '<strong><em><u>$3</u></em></strong>');
|
||||
|
||||
// Bold + Italic (***text***)
|
||||
parsed = parsed.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||||
// Bold + Italic (***text***)
|
||||
parsed = parsed.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||||
|
||||
// Bold + Underline (__**text**__ or **__text__**)
|
||||
parsed = parsed.replace(/__\*\*([^*_]+)\*\*__/g, '<u><strong>$1</strong></u>');
|
||||
parsed = parsed.replace(/\*\*__([^*_]+)__\*\*/g, '<strong><u>$1</u></strong>');
|
||||
// Bold + Underline (__**text**__ or **__text__**)
|
||||
parsed = parsed.replace(/__\*\*([^*_]+)\*\*__/g, '<u><strong>$1</strong></u>');
|
||||
parsed = parsed.replace(/\*\*__([^*_]+)__\*\*/g, '<strong><u>$1</u></strong>');
|
||||
|
||||
// Italic + Underline (__*text*__ or *__text__* or ___text___)
|
||||
parsed = parsed.replace(/__\*([^*_]+)\*__/g, '<u><em>$1</em></u>');
|
||||
parsed = parsed.replace(/\*__([^*_]+)__\*/g, '<em><u>$1</u></em>');
|
||||
parsed = parsed.replace(/___([^_]+)___/g, '<u><em>$1</em></u>');
|
||||
// Italic + Underline (__*text*__ or *__text__* or ___text___)
|
||||
parsed = parsed.replace(/__\*([^*_]+)\*__/g, '<u><em>$1</em></u>');
|
||||
parsed = parsed.replace(/\*__([^*_]+)__\*/g, '<em><u>$1</u></em>');
|
||||
parsed = parsed.replace(/___([^_]+)___/g, '<u><em>$1</em></u>');
|
||||
|
||||
// Bold (**)
|
||||
parsed = parsed.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
// Bold (**)
|
||||
parsed = parsed.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
|
||||
// Underline (__) - must come before italic _ handling
|
||||
parsed = parsed.replace(/__([^_]+)__/g, '<u>$1</u>');
|
||||
// Underline (__) - must come before italic _ handling
|
||||
parsed = parsed.replace(/__([^_]+)__/g, '<u>$1</u>');
|
||||
|
||||
// Italic (* or _)
|
||||
parsed = parsed.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
parsed = parsed.replace(/\b_([^_]+)_\b/g, '<em>$1</em>');
|
||||
// Italic (* or _)
|
||||
parsed = parsed.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
parsed = parsed.replace(/\b_([^_]+)_\b/g, '<em>$1</em>');
|
||||
|
||||
// Strikethrough (~~)
|
||||
parsed = parsed.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||||
// Strikethrough (~~)
|
||||
parsed = parsed.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||||
|
||||
// Spoiler (||)
|
||||
parsed = parsed.replace(/\|\|([^|]+)\|\|/g, '<span class="discord-spoiler">$1</span>');
|
||||
// Spoiler (||)
|
||||
parsed = parsed.replace(/\|\|([^|]+)\|\|/g, '<span class="discord-spoiler">$1</span>');
|
||||
|
||||
// Discord Timestamps (<t:1234567890:F>)
|
||||
parsed = parsed.replace(/<t:(\d+)(?::([tTdDfFR]))?>/g, (_, timestamp, format) => {
|
||||
const date = new Date(parseInt(timestamp) * 1000);
|
||||
let formatted;
|
||||
switch (format) {
|
||||
case 't': // Short time (9:30 PM)
|
||||
formatted = date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
||||
break;
|
||||
case 'T': // Long time (9:30:00 PM)
|
||||
formatted = date.toLocaleTimeString();
|
||||
break;
|
||||
case 'd': // Short date (11/28/2024)
|
||||
formatted = date.toLocaleDateString();
|
||||
break;
|
||||
case 'D': // Long date (November 28, 2024)
|
||||
formatted = date.toLocaleDateString([], { dateStyle: 'long' });
|
||||
break;
|
||||
case 'f': // Short date/time (November 28, 2024 9:30 PM)
|
||||
default:
|
||||
formatted = date.toLocaleString([], { dateStyle: 'long', timeStyle: 'short' });
|
||||
break;
|
||||
case 'F': // Long date/time (Thursday, November 28, 2024 9:30 PM)
|
||||
formatted = date.toLocaleString([], { dateStyle: 'full', timeStyle: 'short' });
|
||||
break;
|
||||
case 'R': // Relative (2 hours ago)
|
||||
const now = Date.now();
|
||||
const diff = now - date.getTime();
|
||||
const seconds = Math.floor(Math.abs(diff) / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
const months = Math.floor(days / 30);
|
||||
const years = Math.floor(days / 365);
|
||||
// Discord Timestamps (<t:1234567890:F>)
|
||||
parsed = parsed.replace(/<t:(\d+)(?::([tTdDfFR]))?>/g, (_, timestamp, format) => {
|
||||
const date = new Date(parseInt(timestamp) * 1000);
|
||||
let formatted;
|
||||
switch (format) {
|
||||
case 't': // Short time (9:30 PM)
|
||||
formatted = date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
||||
break;
|
||||
case 'T': // Long time (9:30:00 PM)
|
||||
formatted = date.toLocaleTimeString();
|
||||
break;
|
||||
case 'd': // Short date (11/28/2024)
|
||||
formatted = date.toLocaleDateString();
|
||||
break;
|
||||
case 'D': // Long date (November 28, 2024)
|
||||
formatted = date.toLocaleDateString([], { dateStyle: 'long' });
|
||||
break;
|
||||
case 'f': // Short date/time (November 28, 2024 9:30 PM)
|
||||
default:
|
||||
formatted = date.toLocaleString([], { dateStyle: 'long', timeStyle: 'short' });
|
||||
break;
|
||||
case 'F': // Long date/time (Thursday, November 28, 2024 9:30 PM)
|
||||
formatted = date.toLocaleString([], { dateStyle: 'full', timeStyle: 'short' });
|
||||
break;
|
||||
case 'R': // Relative (2 hours ago)
|
||||
const now = Date.now();
|
||||
const diff = now - date.getTime();
|
||||
const seconds = Math.floor(Math.abs(diff) / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
const months = Math.floor(days / 30);
|
||||
const years = Math.floor(days / 365);
|
||||
|
||||
if (diff < 0) {
|
||||
// Future
|
||||
if (years > 0) formatted = `in ${years} year${years > 1 ? 's' : ''}`;
|
||||
else if (months > 0) formatted = `in ${months} month${months > 1 ? 's' : ''}`;
|
||||
else if (days > 0) formatted = `in ${days} day${days > 1 ? 's' : ''}`;
|
||||
else if (hours > 0) formatted = `in ${hours} hour${hours > 1 ? 's' : ''}`;
|
||||
else if (minutes > 0) formatted = `in ${minutes} minute${minutes > 1 ? 's' : ''}`;
|
||||
else formatted = `in ${seconds} second${seconds > 1 ? 's' : ''}`;
|
||||
} else {
|
||||
// Past
|
||||
if (years > 0) formatted = `${years} year${years > 1 ? 's' : ''} ago`;
|
||||
else if (months > 0) formatted = `${months} month${months > 1 ? 's' : ''} ago`;
|
||||
else if (days > 0) formatted = `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
else if (hours > 0) formatted = `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
else if (minutes > 0) formatted = `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||
else formatted = `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return `<span class="discord-timestamp" title="${date.toLocaleString()}">${formatted}</span>`;
|
||||
});
|
||||
|
||||
// User mentions (<@123456789>)
|
||||
parsed = parsed.replace(/<@!?(\d+)>/g, (_, userId) => {
|
||||
const user = usersMap[userId];
|
||||
const displayName = user?.displayName || user?.username || 'User';
|
||||
const colorStyle = user?.color ? ` style="color: ${user.color}"` : '';
|
||||
return `<span class="discord-mention"${colorStyle}>@${displayName}</span>`;
|
||||
});
|
||||
|
||||
// @everyone and @here mentions
|
||||
parsed = parsed.replace(/@(everyone|here)/g, '<span class="discord-mention discord-mention-everyone">@$1</span>');
|
||||
|
||||
// Channel mentions (<#123456789>)
|
||||
parsed = parsed.replace(/<#(\d+)>/g, (_, channelId) => {
|
||||
const channel = channelMap.get(channelId);
|
||||
const channelName = channel?.name || 'channel';
|
||||
if (channel) {
|
||||
return `<a href="#" class="discord-mention discord-channel-link" data-channel-id="${channelId}">#${channelName}</a>`;
|
||||
}
|
||||
return `<span class="discord-mention">#${channelName}</span>`;
|
||||
});
|
||||
|
||||
// Role mentions (<@&123456789>) - robust lookup to avoid errors when rolesMap is missing or malformed
|
||||
parsed = parsed.replace(/<@&(\d+)>/g, (_, roleId) => {
|
||||
try {
|
||||
let role = null;
|
||||
if (rolesMap) {
|
||||
if (typeof rolesMap.get === 'function') {
|
||||
role = rolesMap.get(roleId);
|
||||
} else if (rolesMap[roleId]) {
|
||||
role = rolesMap[roleId];
|
||||
} else if (rolesMap[String(roleId)]) {
|
||||
role = rolesMap[String(roleId)];
|
||||
}
|
||||
if (diff < 0) {
|
||||
// Future
|
||||
if (years > 0) formatted = `in ${years} year${years > 1 ? 's' : ''}`;
|
||||
else if (months > 0) formatted = `in ${months} month${months > 1 ? 's' : ''}`;
|
||||
else if (days > 0) formatted = `in ${days} day${days > 1 ? 's' : ''}`;
|
||||
else if (hours > 0) formatted = `in ${hours} hour${hours > 1 ? 's' : ''}`;
|
||||
else if (minutes > 0) formatted = `in ${minutes} minute${minutes > 1 ? 's' : ''}`;
|
||||
else formatted = `in ${seconds} second${seconds > 1 ? 's' : ''}`;
|
||||
} else {
|
||||
// Past
|
||||
if (years > 0) formatted = `${years} year${years > 1 ? 's' : ''} ago`;
|
||||
else if (months > 0) formatted = `${months} month${months > 1 ? 's' : ''} ago`;
|
||||
else if (days > 0) formatted = `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
else if (hours > 0) formatted = `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
else if (minutes > 0) formatted = `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||
else formatted = `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const roleName = role?.name || 'role';
|
||||
const roleColor = role?.color || null;
|
||||
const style = roleColor ? ` style="color: ${roleColor}; background-color: ${roleColor}20;"` : '';
|
||||
return `<span class="discord-mention discord-role-mention"${style}>@${roleName}</span>`;
|
||||
} catch (err) {
|
||||
// Defensive: log for telemetry/debug and return safe fallback
|
||||
try { console.error('parseDiscordMarkdown: role mention parse failed', { roleId, err, rolesMapType: rolesMap && typeof rolesMap }); } catch (e) { /* ignore logging errors */ }
|
||||
return `<span class="discord-mention discord-role-mention">@role</span>`;
|
||||
}
|
||||
});
|
||||
return `<span class="discord-timestamp" title="${date.toLocaleString()}">${formatted}</span>`;
|
||||
});
|
||||
|
||||
// Slash command mentions (</command:123456789>)
|
||||
parsed = parsed.replace(/<\/([^:]+):(\d+)>/g, '<span class="discord-slash-command">/$1</span>');
|
||||
// User mentions (<@123456789>)
|
||||
parsed = parsed.replace(/<@!?(\d+)>/g, (_, userId) => {
|
||||
const user = usersMap[userId];
|
||||
const displayName = user?.displayName || user?.username || 'User';
|
||||
const colorStyle = user?.color ? ` style="color: ${user.color}"` : '';
|
||||
return `<span class="discord-mention"${colorStyle}>@${displayName}</span>`;
|
||||
});
|
||||
|
||||
// Custom emoji (<:name:123456789> or <a:name:123456789>)
|
||||
// Use cached emoji URL if available, otherwise fall back to Discord CDN
|
||||
parsed = parsed.replace(/<(a)?:(\w+):(\d+)>/g, (_, animated, name, id) => {
|
||||
const cached = emojiCache[id];
|
||||
const url = cached?.url || `https://cdn.discordapp.com/emojis/${id}.${animated ? 'gif' : 'png'}`;
|
||||
return `<img class="discord-emoji" src="${url}" alt=":${name}:" title=":${name}:">`;
|
||||
});
|
||||
// @everyone and @here mentions
|
||||
parsed = parsed.replace(/@(everyone|here)/g, '<span class="discord-mention discord-mention-everyone">@$1</span>');
|
||||
|
||||
// Unicode emoji (keep as-is, they render natively)
|
||||
// Channel mentions (<#123456789>)
|
||||
parsed = parsed.replace(/<#(\d+)>/g, (_, channelId) => {
|
||||
const channel = channelMap.get(channelId);
|
||||
const channelName = channel?.name || 'channel';
|
||||
if (channel) {
|
||||
return `<a href="#" class="discord-mention discord-channel-link" data-channel-id="${channelId}">#${channelName}</a>`;
|
||||
}
|
||||
return `<span class="discord-mention">#${channelName}</span>`;
|
||||
});
|
||||
|
||||
// Masked/Markdown links [text](url) or [text](url "title") - process before bare URL detection
|
||||
parsed = parsed.replace(
|
||||
/\[([^\]]+)\]\((https?:\/\/[^\s)"]+)(?:\s+"([^"]+)")?\)/g,
|
||||
(_, text, url, title) => {
|
||||
const titleAttr = title ? ` title="${title}"` : '';
|
||||
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="discord-link"${titleAttr}>${text}</a>`;
|
||||
}
|
||||
);
|
||||
// Role mentions (<@&123456789>) - robust lookup to avoid errors when rolesMap is missing or malformed
|
||||
parsed = parsed.replace(/<@&(\d+)>/g, (_, roleId) => {
|
||||
try {
|
||||
let role = null;
|
||||
if (rolesMap) {
|
||||
if (typeof rolesMap.get === 'function') {
|
||||
role = rolesMap.get(roleId);
|
||||
} else if (rolesMap[roleId]) {
|
||||
role = rolesMap[roleId];
|
||||
} else if (rolesMap[String(roleId)]) {
|
||||
role = rolesMap[String(roleId)];
|
||||
}
|
||||
}
|
||||
const roleName = role?.name || 'role';
|
||||
const roleColor = role?.color || null;
|
||||
const style = roleColor ? ` style="color: ${roleColor}; background-color: ${roleColor}20;"` : '';
|
||||
return `<span class="discord-mention discord-role-mention"${style}>@${roleName}</span>`;
|
||||
} catch (err) {
|
||||
// Defensive: log for telemetry/debug and return safe fallback
|
||||
try { console.error('parseDiscordMarkdown: role mention parse failed', { roleId, err, rolesMapType: rolesMap && typeof rolesMap }); } catch (e) { /* ignore logging errors */ }
|
||||
return `<span class="discord-mention discord-role-mention">@role</span>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Links (URLs) - use negative lookbehind to skip URLs in HTML attributes (src=", href=")
|
||||
// Match URLs that are NOT preceded by =" or ='
|
||||
parsed = parsed.replace(
|
||||
/(?<![="'])(?<![=]["'])(https?:\/\/[^\s<>"']+)/g,
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="discord-link">$1</a>'
|
||||
);
|
||||
// Slash command mentions (</command:123456789>)
|
||||
parsed = parsed.replace(/<\/([^:]+):(\d+)>/g, '<span class="discord-slash-command">/$1</span>');
|
||||
|
||||
// Newlines
|
||||
parsed = parsed.replace(/\n/g, '<br>');
|
||||
// Custom emoji (<:name:123456789> or <a:name:123456789>)
|
||||
// Use cached emoji URL if available, otherwise fall back to Discord CDN
|
||||
parsed = parsed.replace(/<(a)?:(\w+):(\d+)>/g, (_, animated, name, id) => {
|
||||
const cached = emojiCache[id];
|
||||
const url = cached?.url || `https://cdn.discordapp.com/emojis/${id}.${animated ? 'gif' : 'png'}`;
|
||||
return `<img class="discord-emoji" src="${url}" alt=":${name}:" title=":${name}:">`;
|
||||
});
|
||||
|
||||
// Unescape Discord markdown escape sequences (\\_ \\* \\~ \\` \\| \\\\)
|
||||
// Must be done after all markdown processing
|
||||
parsed = parsed.replace(/\\([_*~`|\\])/g, '$1');
|
||||
// Unicode emoji (keep as-is, they render natively)
|
||||
|
||||
return parsed;
|
||||
// Masked/Markdown links [text](url) or [text](url "title") - process before bare URL detection
|
||||
parsed = parsed.replace(
|
||||
/\[([^\]]+)\]\((https?:\/\/[^\s)"]+)(?:\s+"([^"]+)")?\)/g,
|
||||
(_, text, url, title) => {
|
||||
const titleAttr = title ? ` title="${title}"` : '';
|
||||
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="discord-link"${titleAttr}>${text}</a>`;
|
||||
}
|
||||
);
|
||||
|
||||
// Links (URLs) - use negative lookbehind to skip URLs in HTML attributes (src=", href=")
|
||||
// Match URLs that are NOT preceded by =" or ='
|
||||
parsed = parsed.replace(
|
||||
/(?<![="'])(?<![=]["'])(https?:\/\/[^\s<>"']+)/g,
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="discord-link">$1</a>'
|
||||
);
|
||||
|
||||
// Newlines
|
||||
parsed = parsed.replace(/\n/g, '<br>');
|
||||
|
||||
// Unescape Discord markdown escape sequences (\\_ \\* \\~ \\` \\| \\\\)
|
||||
// Must be done after all markdown processing
|
||||
parsed = parsed.replace(/\\([_*~`|\\])/g, '$1');
|
||||
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
try { console.error('parseDiscordMarkdown failed', err); } catch (e) { /* ignore logging errors */ }
|
||||
// Fallback: return a safely-escaped version of the input to avoid crashing the UI
|
||||
|
||||
@@ -19,46 +19,46 @@ const ReqForm = lazy(() => import('./req/ReqForm.jsx'));
|
||||
|
||||
// Inline minimal JoyUI wrapper to avoid importing Components.jsx
|
||||
function MinimalJoyWrapper({ children }) {
|
||||
const cache = React.useRef();
|
||||
if (!cache.current) {
|
||||
cache.current = createCache({ key: "joy-sub" });
|
||||
}
|
||||
return (
|
||||
<CacheProvider value={cache.current}>
|
||||
<CssVarsProvider>{children}</CssVarsProvider>
|
||||
</CacheProvider>
|
||||
);
|
||||
const cache = React.useRef();
|
||||
if (!cache.current) {
|
||||
cache.current = createCache({ key: "joy-sub" });
|
||||
}
|
||||
return (
|
||||
<CacheProvider value={cache.current}>
|
||||
<CssVarsProvider>{children}</CssVarsProvider>
|
||||
</CacheProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SubsiteRoot({ child, ...props }) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.toast = toast;
|
||||
}
|
||||
const theme = typeof document !== 'undefined'
|
||||
? document.documentElement.getAttribute("data-theme")
|
||||
: 'light';
|
||||
if (typeof window !== 'undefined') {
|
||||
window.toast = toast;
|
||||
}
|
||||
const theme = typeof document !== 'undefined'
|
||||
? document.documentElement.getAttribute("data-theme")
|
||||
: 'light';
|
||||
|
||||
return (
|
||||
<PrimeReactProvider>
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={true}
|
||||
closeOnClick={true}
|
||||
rtl={false}
|
||||
pauseOnFocusLoss={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||
/>
|
||||
<MinimalJoyWrapper>
|
||||
{child === "ReqForm" && (
|
||||
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
|
||||
<ReqForm {...props} />
|
||||
</Suspense>
|
||||
)}
|
||||
</MinimalJoyWrapper>
|
||||
</PrimeReactProvider>
|
||||
);
|
||||
return (
|
||||
<PrimeReactProvider>
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={true}
|
||||
closeOnClick={true}
|
||||
rtl={false}
|
||||
pauseOnFocusLoss={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||
/>
|
||||
<MinimalJoyWrapper>
|
||||
{child === "ReqForm" && (
|
||||
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
|
||||
<ReqForm {...props} />
|
||||
</Suspense>
|
||||
)}
|
||||
</MinimalJoyWrapper>
|
||||
</PrimeReactProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user