UNPKG

discord-html-transcripts-fix

Version:

A nicely formatted html transcript generator for discord.js. Bugfix fork with support for the latest discord.js and Components v2.

359 lines (328 loc) 16.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RenderType = void 0; exports.default = MessageContent; exports.MessageSingleASTNode = MessageSingleASTNode; exports.getChannelType = getChannelType; const jsx_runtime_1 = require("react/jsx-runtime"); const discord_markdown_parser_1 = __importDefault(require("discord-markdown-parser")); const discord_js_1 = require("discord.js"); const utils_1 = require("../../utils/utils"); const DiscordHighlightedCode_1 = require("./components/DiscordHighlightedCode"); var RenderType; (function (RenderType) { RenderType[RenderType["EMBED"] = 0] = "EMBED"; RenderType[RenderType["REPLY"] = 1] = "REPLY"; RenderType[RenderType["NORMAL"] = 2] = "NORMAL"; RenderType[RenderType["WEBHOOK"] = 3] = "WEBHOOK"; })(RenderType || (exports.RenderType = RenderType = {})); function hexToRgba(hex, alpha) { if (typeof hex !== 'string') return null; let h = hex.replace(/^#/, ''); if (h.length === 3) h = h.split('').map(c => c + c).join(''); if (!/^[0-9a-fA-F]{6}$/.test(h)) return null; const r = parseInt(h.slice(0, 2), 16); const g = parseInt(h.slice(2, 4), 16); const b = parseInt(h.slice(4, 6), 16); return `rgba(${r},${g},${b},${alpha})`; } // Discord-specific block-level markdown that discord-markdown-parser misses in inline mode. // Run BEFORE the parser, extract heading/subtext lines, parse the rest, splice back together. const HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/; const SUBTEXT_RE = /^-#\s+(.+?)\s*$/; const CODE_FENCE_RE = /^```([a-zA-Z0-9_+-]*)\s*\n([\s\S]*?)```$/m; function preParseDiscord(content, mode) { if (!content || typeof content !== 'string') return []; const lines = content.split('\n'); const nodes = []; let inlineBuffer = ''; let codeFenceActive = null; // {lang, lines} const flushInline = () => { if (!inlineBuffer) return; const parsed = discord_markdown_parser_1.default(inlineBuffer, mode); const arr = Array.isArray(parsed) ? parsed : [parsed]; nodes.push(...arr); inlineBuffer = ''; }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Code fence — close only on `^\s*\`\`\`\s*$` (Discord-style: triple-backticks alone on line) if (codeFenceActive) { if (/^\s*```\s*$/.test(line)) { nodes.push({ type: 'codeBlock', lang: codeFenceActive.lang || '', content: codeFenceActive.lines.join('\n') }); codeFenceActive = null; } else { codeFenceActive.lines.push(line); } continue; } const fenceMatch = line.match(/^```([a-zA-Z0-9_+-]*)\s*$/); if (fenceMatch) { flushInline(); codeFenceActive = { lang: fenceMatch[1], lines: [] }; continue; } const h = line.match(HEADING_RE); if (h) { flushInline(); const level = h[1].length; const headingInner = discord_markdown_parser_1.default(h[2], mode); nodes.push({ type: 'heading', level, content: Array.isArray(headingInner) ? headingInner : [headingInner] }); continue; } const s = line.match(SUBTEXT_RE); if (s) { flushInline(); const innerNodes = discord_markdown_parser_1.default(s[1], mode); nodes.push({ type: 'subtext', content: Array.isArray(innerNodes) ? innerNodes : [innerNodes] }); continue; } if (inlineBuffer) inlineBuffer += '\n'; inlineBuffer += line; } if (codeFenceActive) { // unclosed fence — flush as text-ish inlineBuffer += '```' + (codeFenceActive.lang ? codeFenceActive.lang : '') + '\n' + codeFenceActive.lines.join('\n'); } flushInline(); return nodes; } async function MessageContent({ content, context }) { if (context.type === RenderType.REPLY && content.length > 180) { content = content.slice(0, 180) + '...'; } const mode = context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal'; // REPLY mode is always inline-only (heading inside a reply preview makes no sense). // For all other modes (NORMAL/EMBED/WEBHOOK) we pre-process headings/subtext/code-fences // so they render correctly even though discord-markdown-parser is inline-only. const parsed = context.type === RenderType.REPLY ? discord_markdown_parser_1.default(content, mode) : preParseDiscord(content, mode); const nodes = Array.isArray(parsed) ? parsed : [parsed]; const isOnlyEmojis = nodes.every((node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0)); if (isOnlyEmojis) { const emojis = nodes.filter((node) => ['emoji', 'twemoji'].includes(node.type)); if (emojis.length <= 25) { context._internal = { largeEmojis: true }; } } return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes, context }); } async function MessageASTNodes({ nodes, context }) { if (Array.isArray(nodes)) { return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: nodes.map((node, i) => (0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node, context }, i)) }); } return (0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node: nodes, context }); } async function MessageSingleASTNode({ node, context }) { if (!node) return null; try { return await renderASTNode(node, context); } catch (err) { console.warn('[discord-html-transcripts-fix] AST node render failed:', err && err.message ? err.message : err); try { if (typeof node.content === 'string') return node.content; if (Array.isArray(node.content)) return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }); } catch (_e) {} return null; } } const GUIDE_LABELS = { 'guide': 'Server Guide', 'browse': 'Channels & Roles', 'customize': 'Customize', 'welcome': 'Welcome', 'linked-roles': 'Linked Roles', 'home': 'Home', 'members': 'Members', 'emoji': 'Emoji', 'soundboard': 'Soundboard', 'shop': 'Shop', 'events': 'Events', 'channel': 'Channels', }; async function renderASTNode(node, context) { const type = node.type; switch (type) { case 'text': return node.content; case 'link': return (0, jsx_runtime_1.jsx)("discord-link", { href: (0, utils_1.safeHref)(node.target), children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'url': case 'autolink': return (0, jsx_runtime_1.jsx)("discord-link", { href: (0, utils_1.safeHref)(node.target), target: "_blank", rel: "noreferrer", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'blockQuote': if (context.type === RenderType.REPLY) { return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }); } return (0, jsx_runtime_1.jsx)("discord-quote", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'br': case 'newline': if (context.type === RenderType.REPLY) return ' '; return (0, jsx_runtime_1.jsx)("br", {}); // Headings: #/##/### in Discord. The discord-markdown-parser doesn't emit these // in inline mode, so preParseDiscord extracts them before the parser sees them. case 'heading': { const level = Math.max(1, Math.min(3, node.level || 1)); const Tag = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3'; return (0, jsx_runtime_1.jsx)(Tag, { className: `dht-heading dht-heading-${level}`, children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); } // Subtext: lines starting with `-# ` are small/dim text case 'subtext': return (0, jsx_runtime_1.jsx)("small", { className: "dht-subtext", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'channel': { const id = node.id; if (typeof id === 'string' && /^[a-z][a-z-]*$/.test(id)) { const label = GUIDE_LABELS[id] || id; return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--channel", children: '#' + label }); } const channel = await Promise.resolve(context.callbacks.resolveChannel(id)).catch(() => null); const name = channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `unknown-${(id || '').slice(-6)}`; return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--channel", "data-mention-type": "channel", "data-mention-id": id, children: '#' + name }); } case 'role': { const id = node.id; const role = await Promise.resolve(context.callbacks.resolveRole(id)).catch(() => null); const name = role?.name ?? `role-${(id || '').slice(-6)}`; const color = role?.hexColor && role.hexColor !== '#000000' ? (0, utils_1.safeColor)(role.hexColor, undefined) : undefined; const bg = color ? hexToRgba(color, 0.18) : undefined; const style = color ? { color, backgroundColor: bg, borderColor: hexToRgba(color, 0.45) } : undefined; return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--role", "data-mention-type": "role", "data-mention-id": id, style, children: '@' + name }); } case 'user': { const id = node.id; const user = await Promise.resolve(context.callbacks.resolveUser(id)).catch(() => null); const name = user ? (user.displayName ?? user.username ?? `user-${(id || '').slice(-6)}`) : `user-${(id || '').slice(-6)}`; return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--user", "data-mention-type": "user", "data-mention-id": id, children: '@' + name }); } case 'slashCommand': case 'command': { const name = node.name || node.content || 'command'; return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--command", children: '/' + String(name) }); } case 'guildNavigation': { const id = node.id || node.content || 'guide'; const label = GUIDE_LABELS[id] || id; return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--channel", children: '#' + label }); } case 'here': case 'everyone': return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--role dht-mention--highlight", children: '@' + type }); case 'codeBlock': if (context.type !== RenderType.REPLY) { return (0, jsx_runtime_1.jsx)(DiscordHighlightedCode_1.DiscordHighlightedCode, { language: node.lang, content: node.content }); } return (0, jsx_runtime_1.jsx)("code", { className: "dht-inline-code", children: node.content }); case 'inlineCode': return (0, jsx_runtime_1.jsx)("code", { className: "dht-inline-code", children: node.content }); case 'em': return (0, jsx_runtime_1.jsx)("em", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'strong': return (0, jsx_runtime_1.jsx)("strong", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'underline': return (0, jsx_runtime_1.jsx)("u", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'strikethrough': return (0, jsx_runtime_1.jsx)("s", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'emoticon': return typeof node.content === 'string' ? node.content : (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }); case 'spoiler': return (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) }); case 'emoji': case 'twemoji': { const url = (0, utils_1.parseDiscordEmoji)(node); const jumbo = context._internal?.largeEmojis; return (0, jsx_runtime_1.jsx)("img", { className: jumbo ? "dht-emoji dht-emoji--jumbo" : "dht-emoji", src: url, alt: node.name || '', draggable: false }); } case 'timestamp': { const raw = node.timestamp; let dateMs = NaN; if (typeof raw === 'number') { dateMs = raw < 1e12 ? raw * 1000 : raw; } else if (typeof raw === 'string') { const n = parseInt(raw, 10); if (Number.isFinite(n)) dateMs = n < 1e12 ? n * 1000 : n; } const dt = new Date(dateMs); if (!Number.isFinite(dt.getTime())) return (0, jsx_runtime_1.jsx)("span", { className: "dht-timestamp", children: "—" }); const fmt = formatTimestamp(dt, node.format); return (0, jsx_runtime_1.jsx)("time", { className: "dht-timestamp", dateTime: dt.toISOString(), title: dt.toISOString(), children: fmt }); } default: return typeof node.content === 'string' ? node.content : (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }); } } function formatTimestamp(dt, format) { // Discord formats: t (short time), T (long time), d (short date), D (long date), f (short datetime), F (long datetime), R (relative) try { if (format === 'R') { const diff = (dt.getTime() - Date.now()) / 1000; const abs = Math.abs(diff); const past = diff < 0; const pluralize = (n, unit) => Math.round(n) + ' ' + unit + (Math.round(n) !== 1 ? 's' : ''); let val; if (abs < 60) val = pluralize(abs, 'second'); else if (abs < 3600) val = pluralize(abs / 60, 'minute'); else if (abs < 86400) val = pluralize(abs / 3600, 'hour'); else if (abs < 2592000) val = pluralize(abs / 86400, 'day'); else if (abs < 31536000) val = pluralize(abs / 2592000, 'month'); else val = pluralize(abs / 31536000, 'year'); return past ? `${val} ago` : `in ${val}`; } switch (format) { case 't': return dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); case 'T': return dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); case 'd': return dt.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric' }); case 'D': return dt.toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' }); case 'F': return dt.toLocaleString('en-US', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); case 'f': default: return dt.toLocaleString('en-US', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } } catch (_e) { return dt.toISOString(); } } function getChannelType(channelType) { switch (channelType) { case discord_js_1.ChannelType.GuildCategory: case discord_js_1.ChannelType.GuildAnnouncement: case discord_js_1.ChannelType.GuildText: case discord_js_1.ChannelType.DM: return 'channel'; case discord_js_1.ChannelType.GuildVoice: case discord_js_1.ChannelType.GuildStageVoice: return 'voice'; case discord_js_1.ChannelType.AnnouncementThread: case discord_js_1.ChannelType.PublicThread: case discord_js_1.ChannelType.PrivateThread: return 'thread'; case discord_js_1.ChannelType.GuildForum: return 'forum'; default: return 'channel'; } } //# sourceMappingURL=content.js.map