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 `:${name}:`; - }); + // @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 `:${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 `${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...}> + + + )} + + + ); }