diff --git a/src/components/DiscordLogs.jsx b/src/components/DiscordLogs.jsx
index 54c08fd..cdd6f4c 100644
--- a/src/components/DiscordLogs.jsx
+++ b/src/components/DiscordLogs.jsx
@@ -443,226 +443,226 @@ function parseDiscordMarkdown(text, options = {}) {
.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 `
${trimmedCode}
`;
- });
+ // 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 `${trimmedCode}
`;
+ });
- // Inline code (`) - must be early to prevent formatting inside code
- parsed = parsed.replace(/`([^`]+)`/g, '$1');
+ // Inline code (`) - must be early to prevent formatting inside code
+ parsed = parsed.replace(/`([^`]+)`/g, '$1');
- // 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}${content}
`;
- });
+ // 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}${content}
`;
+ });
- // Headings (# ## ###) - must be at start of line
- // Process before other inline formatting
- parsed = parsed.replace(/(^|\n)### (.+?)(?=\n|$)/gm, (_, before, content) => {
- return `${before}${content}`;
- });
- parsed = parsed.replace(/(^|\n)## (.+?)(?=\n|$)/gm, (_, before, content) => {
- return `${before}${content}`;
- });
- parsed = parsed.replace(/(^|\n)# (.+?)(?=\n|$)/gm, (_, before, content) => {
- return `${before}${content}`;
- });
+ // Headings (# ## ###) - must be at start of line
+ // Process before other inline formatting
+ parsed = parsed.replace(/(^|\n)### (.+?)(?=\n|$)/gm, (_, before, content) => {
+ return `${before}${content}`;
+ });
+ parsed = parsed.replace(/(^|\n)## (.+?)(?=\n|$)/gm, (_, before, content) => {
+ return `${before}${content}`;
+ });
+ parsed = parsed.replace(/(^|\n)# (.+?)(?=\n|$)/gm, (_, before, content) => {
+ return `${before}${content}`;
+ });
- // Subtext/small text (-# at start of line) - process before newline conversion
- parsed = parsed.replace(/(^|\n)-# (.+?)(?=\n|$)/gm, (_, before, content) => {
- return `${before}${content}`;
- });
+ // Subtext/small text (-# at start of line) - process before newline conversion
+ parsed = parsed.replace(/(^|\n)-# (.+?)(?=\n|$)/gm, (_, before, content) => {
+ return `${before}${content}`;
+ });
- // Unordered lists (- or * at start of line, but not ---)
- parsed = parsed.replace(/(^|\n)[-*] (.+?)(?=\n|$)/gm, (_, before, content) => {
- return `${before}• ${content}`;
- });
+ // Unordered lists (- or * at start of line, but not ---)
+ parsed = parsed.replace(/(^|\n)[-*] (.+?)(?=\n|$)/gm, (_, before, content) => {
+ return `${before}• ${content}`;
+ });
- // Ordered lists (1. 2. etc at start of line)
- parsed = parsed.replace(/(^|\n)(\d+)\. (.+?)(?=\n|$)/gm, (_, before, num, content) => {
- return `${before}${num}. ${content}`;
- });
+ // Ordered lists (1. 2. etc at start of line)
+ parsed = parsed.replace(/(^|\n)(\d+)\. (.+?)(?=\n|$)/gm, (_, before, num, content) => {
+ return `${before}${num}. ${content}`;
+ });
- // Bold + Italic + Underline combinations (most specific first)
- // ___***text***___ or ***___text___***
- parsed = parsed.replace(/(\*\*\*|___)(\*\*\*|___)([^*_]+)\2\1/g, '$3');
+ // Bold + Italic + Underline combinations (most specific first)
+ // ___***text***___ or ***___text___***
+ parsed = parsed.replace(/(\*\*\*|___)(\*\*\*|___)([^*_]+)\2\1/g, '$3');
- // Bold + Italic (***text***)
- parsed = parsed.replace(/\*\*\*([^*]+)\*\*\*/g, '$1');
+ // Bold + Italic (***text***)
+ parsed = parsed.replace(/\*\*\*([^*]+)\*\*\*/g, '$1');
- // Bold + Underline (__**text**__ or **__text__**)
- parsed = parsed.replace(/__\*\*([^*_]+)\*\*__/g, '$1');
- parsed = parsed.replace(/\*\*__([^*_]+)__\*\*/g, '$1');
+ // Bold + Underline (__**text**__ or **__text__**)
+ parsed = parsed.replace(/__\*\*([^*_]+)\*\*__/g, '$1');
+ parsed = parsed.replace(/\*\*__([^*_]+)__\*\*/g, '$1');
- // Italic + Underline (__*text*__ or *__text__* or ___text___)
- parsed = parsed.replace(/__\*([^*_]+)\*__/g, '$1');
- parsed = parsed.replace(/\*__([^*_]+)__\*/g, '$1');
- parsed = parsed.replace(/___([^_]+)___/g, '$1');
+ // Italic + Underline (__*text*__ or *__text__* or ___text___)
+ parsed = parsed.replace(/__\*([^*_]+)\*__/g, '$1');
+ parsed = parsed.replace(/\*__([^*_]+)__\*/g, '$1');
+ parsed = parsed.replace(/___([^_]+)___/g, '$1');
- // Bold (**)
- parsed = parsed.replace(/\*\*([^*]+)\*\*/g, '$1');
+ // Bold (**)
+ parsed = parsed.replace(/\*\*([^*]+)\*\*/g, '$1');
- // Underline (__) - must come before italic _ handling
- parsed = parsed.replace(/__([^_]+)__/g, '$1');
+ // Underline (__) - must come before italic _ handling
+ parsed = parsed.replace(/__([^_]+)__/g, '$1');
- // Italic (* or _)
- parsed = parsed.replace(/\*([^*]+)\*/g, '$1');
- parsed = parsed.replace(/\b_([^_]+)_\b/g, '$1');
+ // Italic (* or _)
+ parsed = parsed.replace(/\*([^*]+)\*/g, '$1');
+ parsed = parsed.replace(/\b_([^_]+)_\b/g, '$1');
- // Strikethrough (~~)
- parsed = parsed.replace(/~~([^~]+)~~/g, '$1');
+ // Strikethrough (~~)
+ parsed = parsed.replace(/~~([^~]+)~~/g, '$1');
- // Spoiler (||)
- parsed = parsed.replace(/\|\|([^|]+)\|\|/g, '$1');
+ // Spoiler (||)
+ parsed = parsed.replace(/\|\|([^|]+)\|\|/g, '$1');
- // Discord Timestamps ()
- 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 ()
+ 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 `${formatted}`;
- });
-
- // 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 `@${displayName}`;
- });
-
- // @everyone and @here mentions
- parsed = parsed.replace(/@(everyone|here)/g, '@$1');
-
- // Channel mentions (<#123456789>)
- parsed = parsed.replace(/<#(\d+)>/g, (_, channelId) => {
- const channel = channelMap.get(channelId);
- const channelName = channel?.name || 'channel';
- if (channel) {
- return `#${channelName}`;
- }
- return `#${channelName}`;
- });
-
- // 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 `@${roleName}`;
- } 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 `@role`;
- }
- });
+ return `${formatted}`;
+ });
- // Slash command mentions ()
- parsed = parsed.replace(/<\/([^:]+):(\d+)>/g, '/$1');
+ // 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 `@${displayName}`;
+ });
- // Custom emoji (<:name:123456789> or )
- // 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 `
`;
- });
+ // @everyone and @here mentions
+ parsed = parsed.replace(/@(everyone|here)/g, '@$1');
- // 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 `#${channelName}`;
+ }
+ return `#${channelName}`;
+ });
- // 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 `${text}`;
- }
- );
+ // 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 `@${roleName}`;
+ } 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 `@role`;
+ }
+ });
- // Links (URLs) - use negative lookbehind to skip URLs in HTML attributes (src=", href=")
- // Match URLs that are NOT preceded by =" or ='
- parsed = parsed.replace(
- /(?"']+)/g,
- '$1'
- );
+ // Slash command mentions ()
+ parsed = parsed.replace(/<\/([^:]+):(\d+)>/g, '/$1');
- // Newlines
- parsed = parsed.replace(/\n/g, '
');
+ // Custom emoji (<:name:123456789> or )
+ // 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 `
`;
+ });
- // 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 `${text}`;
+ }
+ );
+
+ // Links (URLs) - use negative lookbehind to skip URLs in HTML attributes (src=", href=")
+ // Match URLs that are NOT preceded by =" or ='
+ parsed = parsed.replace(
+ /(?"']+)/g,
+ '$1'
+ );
+
+ // Newlines
+ parsed = parsed.replace(/\n/g, '
');
+
+ // 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
diff --git a/src/components/SubsiteAppLayout.jsx b/src/components/SubsiteAppLayout.jsx
index 1ec7ee7..10fe9d3 100644
--- a/src/components/SubsiteAppLayout.jsx
+++ b/src/components/SubsiteAppLayout.jsx
@@ -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 (
-
- {children}
-
- );
+ const cache = React.useRef();
+ if (!cache.current) {
+ cache.current = createCache({ key: "joy-sub" });
+ }
+ return (
+
+ {children}
+
+ );
}
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 (
-
-
-
- {child === "ReqForm" && (
- Loading...}>
-
-
- )}
-
-
- );
+ return (
+
+
+
+ {child === "ReqForm" && (
+ Loading...}>
+
+
+ )}
+
+
+ );
}