discord-html-transcripts-fix 1.2.0 → 1.4.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,139 @@ 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.jsx)("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 Promise.resolve(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.jsx)("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 Promise.resolve(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.jsx)("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 Promise.resolve(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.jsx)("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.jsx)("span", { className: "dht-mention dht-mention--command", children: '/' + String(name) });
114
242
  }
243
+
244
+ case 'guildNavigation': {
245
+ const id = node.id || node.content || 'guide';
246
+ const label = GUIDE_LABELS[id] || id;
247
+ return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--channel", children: '#' + label });
248
+ }
249
+
115
250
  case 'here':
116
251
  case 'everyone':
117
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: 'role', highlight: true, children: `@${type}` }));
252
+ return (0, jsx_runtime_1.jsx)("span", { className: "dht-mention dht-mention--role dht-mention--highlight", children: '@' + type });
253
+
118
254
  case 'codeBlock':
119
255
  if (context.type !== RenderType.REPLY) {
120
256
  return (0, jsx_runtime_1.jsx)(DiscordHighlightedCode_1.DiscordHighlightedCode, { language: node.lang, content: node.content });
121
257
  }
122
- return (0, jsx_runtime_1.jsx)("discord-code", { children: node.content });
258
+ return (0, jsx_runtime_1.jsx)("code", { className: "dht-inline-code", children: node.content });
123
259
  case 'inlineCode':
124
- return (0, jsx_runtime_1.jsx)("discord-code", { children: node.content });
260
+ return (0, jsx_runtime_1.jsx)("code", { className: "dht-inline-code", children: node.content });
261
+
125
262
  case 'em':
126
- return ((0, jsx_runtime_1.jsx)("discord-italic", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
263
+ return (0, jsx_runtime_1.jsx)("em", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
127
264
  case 'strong':
128
- return ((0, jsx_runtime_1.jsx)("discord-bold", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
265
+ return (0, jsx_runtime_1.jsx)("strong", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
129
266
  case 'underline':
130
- return ((0, jsx_runtime_1.jsx)("discord-underlined", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
267
+ return (0, jsx_runtime_1.jsx)("u", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
131
268
  case 'strikethrough':
132
- return ((0, jsx_runtime_1.jsx)("s", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
269
+ return (0, jsx_runtime_1.jsx)("s", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
133
270
  case 'emoticon':
134
- return typeof node.content === 'string' ? node.content : ((0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }));
271
+ return typeof node.content === 'string' ? node.content : (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
135
272
  case 'spoiler':
136
- return ((0, jsx_runtime_1.jsx)("discord-spoiler", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
273
+ return (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
274
+
137
275
  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 }));
276
+ case 'twemoji': {
277
+ const url = (0, utils_1.parseDiscordEmoji)(node);
278
+ const jumbo = context._internal?.largeEmojis;
279
+ return (0, jsx_runtime_1.jsx)("img", {
280
+ className: jumbo ? "dht-emoji dht-emoji--jumbo" : "dht-emoji",
281
+ src: url,
282
+ alt: node.name || '',
283
+ draggable: false
284
+ });
285
+ }
286
+
140
287
  case 'timestamp': {
141
- // Defensive: invalid <t:...> markers must not throw
142
288
  const raw = node.timestamp;
143
289
  let dateMs = NaN;
144
290
  if (typeof raw === 'number') {
@@ -148,13 +294,44 @@ async function renderASTNode(node, context) {
148
294
  if (Number.isFinite(n)) dateMs = n < 1e12 ? n * 1000 : n;
149
295
  }
150
296
  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 });
297
+ if (!Number.isFinite(dt.getTime())) return (0, jsx_runtime_1.jsx)("span", { className: "dht-timestamp", children: "—" });
298
+ const fmt = formatTimestamp(dt, node.format);
299
+ return (0, jsx_runtime_1.jsx)("time", { className: "dht-timestamp", dateTime: dt.toISOString(), title: dt.toISOString(), children: fmt });
300
+ }
301
+
302
+ default:
303
+ return typeof node.content === 'string' ? node.content : (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
304
+ }
305
+ }
306
+
307
+ function formatTimestamp(dt, format) {
308
+ // Discord formats: t (short time), T (long time), d (short date), D (long date), f (short datetime), F (long datetime), R (relative)
309
+ try {
310
+ if (format === 'R') {
311
+ const diff = (dt.getTime() - Date.now()) / 1000;
312
+ const abs = Math.abs(diff);
313
+ const past = diff < 0;
314
+ const pluralize = (n, unit) => Math.round(n) + ' ' + unit + (Math.round(n) !== 1 ? 's' : '');
315
+ let val;
316
+ if (abs < 60) val = pluralize(abs, 'second');
317
+ else if (abs < 3600) val = pluralize(abs / 60, 'minute');
318
+ else if (abs < 86400) val = pluralize(abs / 3600, 'hour');
319
+ else if (abs < 2592000) val = pluralize(abs / 86400, 'day');
320
+ else if (abs < 31536000) val = pluralize(abs / 2592000, 'month');
321
+ else val = pluralize(abs / 31536000, 'year');
322
+ return past ? `${val} ago` : `in ${val}`;
154
323
  }
155
- default: {
156
- return typeof node.content === 'string' ? node.content : ((0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }));
324
+ switch (format) {
325
+ case 't': return dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
326
+ case 'T': return dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
327
+ case 'd': return dt.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric' });
328
+ case 'D': return dt.toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' });
329
+ case 'F': return dt.toLocaleString('en-US', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
330
+ case 'f':
331
+ default: return dt.toLocaleString('en-US', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
157
332
  }
333
+ } catch (_e) {
334
+ return dt.toISOString();
158
335
  }
159
336
  }
160
337
 
@@ -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,