discord-html-transcripts-fix 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ const discord_markdown_parser_1 = __importDefault(require("discord-markdown-pars
12
12
  const discord_js_1 = require("discord.js");
13
13
  const utils_1 = require("../../utils/utils");
14
14
  const DiscordHighlightedCode_1 = require("./components/DiscordHighlightedCode");
15
+
15
16
  var RenderType;
16
17
  (function (RenderType) {
17
18
  RenderType[RenderType["EMBED"] = 0] = "EMBED";
@@ -20,25 +21,114 @@ var RenderType;
20
21
  RenderType[RenderType["WEBHOOK"] = 3] = "WEBHOOK";
21
22
  })(RenderType || (exports.RenderType = RenderType = {}));
22
23
 
24
+ function hexToRgba(hex, alpha) {
25
+ if (typeof hex !== 'string') return null;
26
+ let h = hex.replace(/^#/, '');
27
+ if (h.length === 3) h = h.split('').map(c => c + c).join('');
28
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
29
+ const r = parseInt(h.slice(0, 2), 16);
30
+ const g = parseInt(h.slice(2, 4), 16);
31
+ const b = parseInt(h.slice(4, 6), 16);
32
+ return `rgba(${r},${g},${b},${alpha})`;
33
+ }
34
+
35
+ // Discord-specific block-level markdown that discord-markdown-parser misses in inline mode.
36
+ // Run BEFORE the parser, extract heading/subtext lines, parse the rest, splice back together.
37
+ const HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
38
+ const SUBTEXT_RE = /^-#\s+(.+?)\s*$/;
39
+ const CODE_FENCE_RE = /^```([a-zA-Z0-9_+-]*)\s*\n([\s\S]*?)```$/m;
40
+
41
+ function preParseDiscord(content, mode) {
42
+ if (!content || typeof content !== 'string') return [];
43
+ const lines = content.split('\n');
44
+ const nodes = [];
45
+ let inlineBuffer = '';
46
+ let codeFenceActive = null; // {lang, lines}
47
+
48
+ const flushInline = () => {
49
+ if (!inlineBuffer) return;
50
+ const parsed = discord_markdown_parser_1.default(inlineBuffer, mode);
51
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
52
+ nodes.push(...arr);
53
+ inlineBuffer = '';
54
+ };
55
+
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i];
58
+
59
+ // Code fence — close only on `^\s*\`\`\`\s*$` (Discord-style: triple-backticks alone on line)
60
+ if (codeFenceActive) {
61
+ if (/^\s*```\s*$/.test(line)) {
62
+ nodes.push({ type: 'codeBlock', lang: codeFenceActive.lang || '', content: codeFenceActive.lines.join('\n') });
63
+ codeFenceActive = null;
64
+ } else {
65
+ codeFenceActive.lines.push(line);
66
+ }
67
+ continue;
68
+ }
69
+ const fenceMatch = line.match(/^```([a-zA-Z0-9_+-]*)\s*$/);
70
+ if (fenceMatch) {
71
+ flushInline();
72
+ codeFenceActive = { lang: fenceMatch[1], lines: [] };
73
+ continue;
74
+ }
75
+
76
+ const h = line.match(HEADING_RE);
77
+ if (h) {
78
+ flushInline();
79
+ const level = h[1].length;
80
+ const headingInner = discord_markdown_parser_1.default(h[2], mode);
81
+ nodes.push({ type: 'heading', level, content: Array.isArray(headingInner) ? headingInner : [headingInner] });
82
+ continue;
83
+ }
84
+
85
+ const s = line.match(SUBTEXT_RE);
86
+ if (s) {
87
+ flushInline();
88
+ const innerNodes = discord_markdown_parser_1.default(s[1], mode);
89
+ nodes.push({ type: 'subtext', content: Array.isArray(innerNodes) ? innerNodes : [innerNodes] });
90
+ continue;
91
+ }
92
+
93
+ if (inlineBuffer) inlineBuffer += '\n';
94
+ inlineBuffer += line;
95
+ }
96
+ if (codeFenceActive) {
97
+ // unclosed fence — flush as text-ish
98
+ inlineBuffer += '```' + (codeFenceActive.lang ? codeFenceActive.lang : '') + '\n' + codeFenceActive.lines.join('\n');
99
+ }
100
+ flushInline();
101
+ return nodes;
102
+ }
103
+
23
104
  async function MessageContent({ content, context }) {
24
- if (context.type === RenderType.REPLY && content.length > 180)
105
+ if (context.type === RenderType.REPLY && content.length > 180) {
25
106
  content = content.slice(0, 180) + '...';
26
- const parsed = (0, discord_markdown_parser_1.default)(content, context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal');
27
- const isOnlyEmojis = parsed.every((node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0));
107
+ }
108
+ const mode = context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal';
109
+ // REPLY mode is always inline-only (heading inside a reply preview makes no sense).
110
+ // For all other modes (NORMAL/EMBED/WEBHOOK) we pre-process headings/subtext/code-fences
111
+ // so they render correctly even though discord-markdown-parser is inline-only.
112
+ const parsed = context.type === RenderType.REPLY
113
+ ? discord_markdown_parser_1.default(content, mode)
114
+ : preParseDiscord(content, mode);
115
+
116
+ const nodes = Array.isArray(parsed) ? parsed : [parsed];
117
+ const isOnlyEmojis = nodes.every((node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0));
28
118
  if (isOnlyEmojis) {
29
- const emojis = parsed.filter((node) => ['emoji', 'twemoji'].includes(node.type));
119
+ const emojis = nodes.filter((node) => ['emoji', 'twemoji'].includes(node.type));
30
120
  if (emojis.length <= 25) {
31
121
  context._internal = { largeEmojis: true };
32
122
  }
33
123
  }
34
- return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: parsed, context: context });
124
+ return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes, context });
35
125
  }
36
126
 
37
127
  async function MessageASTNodes({ nodes, context }) {
38
128
  if (Array.isArray(nodes)) {
39
- return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: nodes.map((node, i) => ((0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node: node, context: context }, i))) }));
129
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: nodes.map((node, i) => (0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node, context }, i)) });
40
130
  }
41
- return (0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node: nodes, context: context });
131
+ return (0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node: nodes, context });
42
132
  }
43
133
 
44
134
  async function MessageSingleASTNode({ node, context }) {
@@ -49,13 +139,12 @@ async function MessageSingleASTNode({ node, context }) {
49
139
  console.warn('[discord-html-transcripts-fix] AST node render failed:', err && err.message ? err.message : err);
50
140
  try {
51
141
  if (typeof node.content === 'string') return node.content;
52
- if (Array.isArray(node.content)) return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context });
142
+ if (Array.isArray(node.content)) return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
53
143
  } catch (_e) {}
54
144
  return null;
55
145
  }
56
146
  }
57
147
 
58
- // Detect Discord "guide" pseudo-channels: <id:guide>, <id:browse>, <id:customize>, <id:welcome>, <id:linked-roles>
59
148
  const GUIDE_LABELS = {
60
149
  'guide': 'Server Guide',
61
150
  'browse': 'Channels & Roles',
@@ -63,82 +152,140 @@ const GUIDE_LABELS = {
63
152
  'welcome': 'Welcome',
64
153
  'linked-roles': 'Linked Roles',
65
154
  'home': 'Home',
155
+ 'members': 'Members',
156
+ 'emoji': 'Emoji',
157
+ 'soundboard': 'Soundboard',
158
+ 'shop': 'Shop',
159
+ 'events': 'Events',
160
+ 'channel': 'Channels',
66
161
  };
67
162
 
68
163
  async function renderASTNode(node, context) {
69
- var _a, _b;
70
164
  const type = node.type;
71
165
  switch (type) {
72
166
  case 'text':
73
167
  return node.content;
74
168
  case 'link':
75
- 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: context }) }));
169
+ 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 }) });
76
170
  case 'url':
77
171
  case 'autolink':
78
- 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: context }) }));
172
+ 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 }) });
79
173
  case 'blockQuote':
80
174
  if (context.type === RenderType.REPLY) {
81
- return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context });
175
+ return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
82
176
  }
83
- return ((0, jsx_runtime_1.jsx)("discord-quote", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
177
+ return (0, jsx_runtime_1.jsx)("discord-quote", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
84
178
  case 'br':
85
179
  case 'newline':
86
180
  if (context.type === RenderType.REPLY) return ' ';
87
181
  return (0, jsx_runtime_1.jsx)("br", {});
182
+
183
+ // Headings: #/##/### in Discord. The discord-markdown-parser doesn't emit these
184
+ // in inline mode, so preParseDiscord extracts them before the parser sees them.
185
+ case 'heading': {
186
+ const level = Math.max(1, Math.min(3, node.level || 1));
187
+ const Tag = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3';
188
+ return (0, jsx_runtime_1.jsx)(Tag, { className: `dht-heading dht-heading-${level}`, children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
189
+ }
190
+ // Subtext: lines starting with `-# ` are small/dim text
191
+ case 'subtext':
192
+ return (0, jsx_runtime_1.jsx)("small", { className: "dht-subtext", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
193
+
88
194
  case 'channel': {
89
195
  const id = node.id;
90
- // Discord pseudo channels (<id:guide>, <id:browse>, …) come through with letters as id
91
196
  if (typeof id === 'string' && /^[a-z][a-z-]*$/.test(id)) {
92
197
  const label = GUIDE_LABELS[id] || id;
93
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: "channel", className: "dht-mention", children: label }));
198
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--channel", children: ["#", label] });
94
199
  }
95
- const channel = await context.callbacks.resolveChannel(id);
96
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: channel ? (channel.isDMBased() ? 'channel' : getChannelType(channel.type)) : 'channel', "data-mention-type": "channel", "data-mention-id": id, className: "dht-mention", children: channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `<#${id}>` }));
200
+ const channel = await context.callbacks.resolveChannel(id).catch(() => null);
201
+ const name = channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `unknown-${id.slice(-6)}`;
202
+ return (0, jsx_runtime_1.jsxs)("span", {
203
+ className: "dht-mention dht-mention--channel",
204
+ "data-mention-type": "channel",
205
+ "data-mention-id": id,
206
+ children: ["#", name]
207
+ });
97
208
  }
209
+
98
210
  case 'role': {
99
211
  const id = node.id;
100
- const role = await context.callbacks.resolveRole(id);
101
- const color = role?.hexColor ? (0, utils_1.safeColor)(role.hexColor, undefined) : undefined;
102
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: "role", color: context.type === RenderType.REPLY ? undefined : color, "data-mention-type": "role", "data-mention-id": id, className: "dht-mention", children: role ? role.name : `<@&${id}>` }));
212
+ const role = await context.callbacks.resolveRole(id).catch(() => null);
213
+ const name = role?.name ?? `role-${id.slice(-6)}`;
214
+ const color = role?.hexColor && role.hexColor !== '#000000' ? (0, utils_1.safeColor)(role.hexColor, undefined) : undefined;
215
+ const bg = color ? hexToRgba(color, 0.18) : undefined;
216
+ const style = color ? { color, backgroundColor: bg, borderColor: hexToRgba(color, 0.45) } : undefined;
217
+ return (0, jsx_runtime_1.jsxs)("span", {
218
+ className: "dht-mention dht-mention--role",
219
+ "data-mention-type": "role",
220
+ "data-mention-id": id,
221
+ style,
222
+ children: ["@", name]
223
+ });
103
224
  }
225
+
104
226
  case 'user': {
105
227
  const id = node.id;
106
- const user = await context.callbacks.resolveUser(id);
107
- return (0, jsx_runtime_1.jsx)("discord-mention", { type: "user", "data-mention-type": "user", "data-mention-id": id, className: "dht-mention", children: user ? ((_a = user.displayName) !== null && _a !== void 0 ? _a : user.username) : `<@${id}>` });
228
+ const user = await context.callbacks.resolveUser(id).catch(() => null);
229
+ const name = user ? (user.displayName ?? user.username ?? `user-${id.slice(-6)}`) : `user-${id.slice(-6)}`;
230
+ return (0, jsx_runtime_1.jsxs)("span", {
231
+ className: "dht-mention dht-mention--user",
232
+ "data-mention-type": "user",
233
+ "data-mention-id": id,
234
+ children: ["@", name]
235
+ });
108
236
  }
237
+
109
238
  case 'slashCommand':
110
239
  case 'command': {
111
- // </name:id> renders as blue pill like Discord
112
240
  const name = node.name || node.content || 'command';
113
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: "command", className: "dht-mention dht-mention--cmd", children: '/' + String(name) }));
241
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--command", children: ["/", String(name)] });
114
242
  }
243
+
244
+ case 'guildNavigation': {
245
+ // discord-markdown-parser emits this for <id:guide> etc.
246
+ const id = node.id || node.content || 'guide';
247
+ const label = GUIDE_LABELS[id] || id;
248
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--channel", children: ["#", label] });
249
+ }
250
+
115
251
  case 'here':
116
252
  case 'everyone':
117
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: 'role', highlight: true, children: `@${type}` }));
253
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--role dht-mention--highlight", children: ["@", type] });
254
+
118
255
  case 'codeBlock':
119
256
  if (context.type !== RenderType.REPLY) {
120
257
  return (0, jsx_runtime_1.jsx)(DiscordHighlightedCode_1.DiscordHighlightedCode, { language: node.lang, content: node.content });
121
258
  }
122
- return (0, jsx_runtime_1.jsx)("discord-code", { children: node.content });
259
+ return (0, jsx_runtime_1.jsx)("code", { className: "dht-inline-code", children: node.content });
123
260
  case 'inlineCode':
124
- return (0, jsx_runtime_1.jsx)("discord-code", { children: node.content });
261
+ return (0, jsx_runtime_1.jsx)("code", { className: "dht-inline-code", children: node.content });
262
+
125
263
  case 'em':
126
- return ((0, jsx_runtime_1.jsx)("discord-italic", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
264
+ return (0, jsx_runtime_1.jsx)("em", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
127
265
  case 'strong':
128
- return ((0, jsx_runtime_1.jsx)("discord-bold", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
266
+ return (0, jsx_runtime_1.jsx)("strong", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
129
267
  case 'underline':
130
- return ((0, jsx_runtime_1.jsx)("discord-underlined", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
268
+ return (0, jsx_runtime_1.jsx)("u", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
131
269
  case 'strikethrough':
132
- return ((0, jsx_runtime_1.jsx)("s", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
270
+ return (0, jsx_runtime_1.jsx)("s", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
133
271
  case 'emoticon':
134
- return typeof node.content === 'string' ? node.content : ((0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }));
272
+ return typeof node.content === 'string' ? node.content : (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
135
273
  case 'spoiler':
136
- return ((0, jsx_runtime_1.jsx)("discord-spoiler", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
274
+ return (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
275
+
137
276
  case 'emoji':
138
- case 'twemoji':
139
- return ((0, jsx_runtime_1.jsx)("discord-custom-emoji", { name: node.name, url: (0, utils_1.parseDiscordEmoji)(node), embedEmoji: context.type === RenderType.EMBED, jumbo: (_b = context._internal) === null || _b === void 0 ? void 0 : _b.largeEmojis }));
277
+ case 'twemoji': {
278
+ const url = (0, utils_1.parseDiscordEmoji)(node);
279
+ const jumbo = context._internal?.largeEmojis;
280
+ return (0, jsx_runtime_1.jsx)("img", {
281
+ className: jumbo ? "dht-emoji dht-emoji--jumbo" : "dht-emoji",
282
+ src: url,
283
+ alt: node.name || '',
284
+ draggable: false
285
+ });
286
+ }
287
+
140
288
  case 'timestamp': {
141
- // Defensive: invalid <t:...> markers must not throw
142
289
  const raw = node.timestamp;
143
290
  let dateMs = NaN;
144
291
  if (typeof raw === 'number') {
@@ -148,13 +295,44 @@ async function renderASTNode(node, context) {
148
295
  if (Number.isFinite(n)) dateMs = n < 1e12 ? n * 1000 : n;
149
296
  }
150
297
  const dt = new Date(dateMs);
151
- let iso = '';
152
- try { iso = Number.isFinite(dt.getTime()) ? dt.toISOString() : ''; } catch (_e) { iso = ''; }
153
- return (0, jsx_runtime_1.jsx)("discord-time", { children: iso });
298
+ if (!Number.isFinite(dt.getTime())) return (0, jsx_runtime_1.jsx)("span", { className: "dht-timestamp", children: "—" });
299
+ const fmt = formatTimestamp(dt, node.format);
300
+ return (0, jsx_runtime_1.jsx)("time", { className: "dht-timestamp", dateTime: dt.toISOString(), title: dt.toISOString(), children: fmt });
301
+ }
302
+
303
+ default:
304
+ return typeof node.content === 'string' ? node.content : (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
305
+ }
306
+ }
307
+
308
+ function formatTimestamp(dt, format) {
309
+ // Discord formats: t (short time), T (long time), d (short date), D (long date), f (short datetime), F (long datetime), R (relative)
310
+ try {
311
+ if (format === 'R') {
312
+ const diff = (dt.getTime() - Date.now()) / 1000;
313
+ const abs = Math.abs(diff);
314
+ const past = diff < 0;
315
+ const pluralize = (n, unit) => Math.round(n) + ' ' + unit + (Math.round(n) !== 1 ? 's' : '');
316
+ let val;
317
+ if (abs < 60) val = pluralize(abs, 'second');
318
+ else if (abs < 3600) val = pluralize(abs / 60, 'minute');
319
+ else if (abs < 86400) val = pluralize(abs / 3600, 'hour');
320
+ else if (abs < 2592000) val = pluralize(abs / 86400, 'day');
321
+ else if (abs < 31536000) val = pluralize(abs / 2592000, 'month');
322
+ else val = pluralize(abs / 31536000, 'year');
323
+ return past ? `${val} ago` : `in ${val}`;
154
324
  }
155
- default: {
156
- return typeof node.content === 'string' ? node.content : ((0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }));
325
+ switch (format) {
326
+ case 't': return dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
327
+ case 'T': return dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
328
+ case 'd': return dt.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric' });
329
+ case 'D': return dt.toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' });
330
+ case 'F': return dt.toLocaleString('en-US', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
331
+ case 'f':
332
+ default: return dt.toLocaleString('en-US', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
157
333
  }
334
+ } catch (_e) {
335
+ return dt.toISOString();
158
336
  }
159
337
  }
160
338
 
@@ -11,16 +11,41 @@ const content_1 = __importStar(require("./content"));
11
11
  const utils_1 = require("../../utils/utils");
12
12
 
13
13
  async function DiscordEmbed({ embed, context }) {
14
+ const type = embed.type || 'rich';
15
+ const url = embed.url ? (0, utils_1.safeHref)(embed.url) : undefined;
16
+ const imageUrl = embed.image?.proxyURL ?? embed.image?.url;
17
+ const videoUrl = embed.video?.proxyURL ?? embed.video?.url;
18
+ const provider = embed.provider?.name;
19
+ const safeVid = videoUrl ? (0, utils_1.safeHref)(videoUrl) : null;
20
+ const safeImg = imageUrl ? (0, utils_1.safeHref)(imageUrl) : null;
21
+
22
+ // Image-only embeds (link previews): render the image alone with optional caption
23
+ if (type === 'image' && safeImg) {
24
+ return (0, jsx_runtime_1.jsx)("div", { className: "dht-embed dht-embed--image", children: (0, jsx_runtime_1.jsx)("a", { href: url || safeImg, target: "_blank", rel: "noreferrer", children: (0, jsx_runtime_1.jsx)("img", { src: safeImg, alt: embed.title || '', className: "dht-embed-image", loading: "lazy" }) }) });
25
+ }
26
+ // GIFV — auto-play loop video
27
+ if (type === 'gifv' && safeVid) {
28
+ return (0, jsx_runtime_1.jsx)("div", { className: "dht-embed dht-embed--gifv", children: (0, jsx_runtime_1.jsx)("video", { src: safeVid, autoPlay: true, loop: true, muted: true, playsInline: true, className: "dht-embed-gifv", "aria-label": embed.title || 'GIF' }) });
29
+ }
30
+ // Video embed (YouTube/Vimeo/etc.) — show thumbnail with a play overlay linking to original
31
+ if (type === 'video' && (safeVid || safeImg)) {
32
+ return (0, jsx_runtime_1.jsxs)("div", { className: "dht-embed dht-embed--video", children: [
33
+ (0, jsx_runtime_1.jsxs)("a", { href: url || safeVid || safeImg, target: "_blank", rel: "noreferrer", className: "dht-embed-video-wrap", children: [
34
+ safeImg && (0, jsx_runtime_1.jsx)("img", { src: safeImg, alt: embed.title || '', className: "dht-embed-video-thumb", loading: "lazy" }),
35
+ (0, jsx_runtime_1.jsx)("div", { className: "dht-embed-video-play", children: '▶' })
36
+ ] }),
37
+ provider && (0, jsx_runtime_1.jsx)("div", { className: "dht-embed-provider", children: provider }),
38
+ embed.title && (0, jsx_runtime_1.jsx)("a", { href: url || safeVid, target: "_blank", rel: "noreferrer", className: "dht-embed-title", children: embed.title }),
39
+ ] });
40
+ }
41
+
42
+ // Default rich embed
14
43
  const title = embed.title ?? undefined;
15
44
  const authorImage = embed.author?.proxyIconURL ?? embed.author?.iconURL;
16
45
  const authorName = embed.author?.name;
17
46
  const authorUrl = embed.author?.url ? (0, utils_1.safeHref)(embed.author.url) : undefined;
18
47
  const color = embed.hexColor ? (0, utils_1.safeColor)(embed.hexColor, undefined) : undefined;
19
- const image = embed.image?.proxyURL ?? embed.image?.url;
20
48
  const thumb = embed.thumbnail?.proxyURL ?? embed.thumbnail?.url;
21
- const url = embed.url ? (0, utils_1.safeHref)(embed.url) : undefined;
22
- const provider = embed.provider?.name;
23
- const video = embed.video?.url ? (0, utils_1.safeHref)(embed.video.url) : null;
24
49
 
25
50
  return ((0, jsx_runtime_1.jsxs)("discord-embed", {
26
51
  "embed-title": title,
@@ -29,7 +54,7 @@ async function DiscordEmbed({ embed, context }) {
29
54
  "author-name": authorName,
30
55
  "author-url": authorUrl,
31
56
  color: color,
32
- image: image,
57
+ image: safeImg,
33
58
  thumbnail: thumb,
34
59
  url: url,
35
60
  children: [
@@ -42,7 +67,7 @@ async function DiscordEmbed({ embed, context }) {
42
67
  slot: "fields",
43
68
  children: embed.fields.map((field, id) => ((0, jsx_runtime_1.jsx)(EmbedField, { embed, field, id, context }, `${context.message.id}-e-${context.index}-f-${id}`)))
44
69
  })),
45
- video && ((0, jsx_runtime_1.jsx)("div", { className: "dht-embed-video", children: (0, jsx_runtime_1.jsx)("a", { href: video, target: "_blank", rel: "noreferrer", children: '▶ Open video' }) })),
70
+ safeVid && ((0, jsx_runtime_1.jsx)("div", { className: "dht-embed-video", children: (0, jsx_runtime_1.jsxs)("a", { href: safeVid, target: "_blank", rel: "noreferrer", children: ['▶ Open video', provider ? ` (${provider})` : ''] }) })),
46
71
  embed.footer && ((0, jsx_runtime_1.jsx)("discord-embed-footer", {
47
72
  slot: "footer",
48
73
  footerImage: embed.footer.proxyIconURL ?? embed.footer.iconURL,
@@ -53,7 +78,6 @@ async function DiscordEmbed({ embed, context }) {
53
78
  }, `${context.message.id}-e-${context.index}`));
54
79
  }
55
80
 
56
- // Extracted as proper async component (was inline async arrow inside .map() — undefined React behavior)
57
81
  async function EmbedField({ embed, field, id, context }) {
58
82
  return (0, jsx_runtime_1.jsx)("discord-embed-field", {
59
83
  "field-title": field.name,