discord-message-transcript-base 1.0.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.
@@ -0,0 +1 @@
1
+ export const MINIFIED_CSS = `body{background-color:#3a3c43;color:#dbdee1;font-family:"Whitney","Helvetica Neue",Helvetica,Arial,sans-serif;margin:0;padding:0;width:100%}header{height:fit-content;border-bottom:2px solid black;margin-top:2rem;padding-left:2rem;padding-bottom:1rem;display:flex;flex-direction:column;display:flex;flex-direction:row}a{text-decoration:none;color:#1E90FF}p{margin:0}h1{margin:.5rem 0}h2{margin:.3rem 0}h3{margin:.15rem}h4{margin:0}code{border:1px solid #202225;border-radius:.25rem}blockquote{margin:.5rem 0;border-left:.25rem solid #4f545c;padding:.4rem .6rem;border-radius:.25rem;color:#9f9fa6}.c1{display:flex;align-items:baseline;gap:.5rem}.c2{background-color:#5865f2;color:white;font-weight:600;font-size:80%;padding:.1rem .35rem;border-radius:.25rem;letter-spacing:.03rem;height:fit-content;width:fit-content;align-self:flex-start}.c3{background-color:#747F8D50;color:white;font-weight:600;font-size:70%;padding:.1rem .35rem;border-radius:.25rem;letter-spacing:.03rem;height:fit-content;width:fit-content;align-self:center}.c4{background-color:#5664fa41;padding:.2rem;border-radius:.25rem;transition:background-color .2s ease}.c4:hover{background-color:#5664fa7e}.c5{width:7rem;height:7rem;border-radius:50%;background-color:#4f545c;display:flex;align-items:center;justify-content:center;font-size:3rem;font-weight:600}.c6{display:flex;flex-direction:column;gap:.2rem;padding:.5rem;border-radius:1rem}.c6.highlight,.c6:hover{background-color:#40434b;transition:background-color .3s ease-in-out}.c7{display:flex;flex-direction:row;gap:1rem;padding:.5rem;border-radius:.25rem}.c8{width:3.5rem;height:3.5rem;border-radius:50%}.c9{display:flex;flex-direction:column;gap:.25rem}.c10{display:flex;flex-direction:row;gap:.75rem}.c11{margin:0}.c12{color:#999;font-size:77.5%;align-self:center}.c13{line-height:1.5}.c14{white-space:pre-wrap}.c15{font-size:85%;color:#808080}.c16{display:inline-block;background-color:#202225;color:#202225;padding:0 .2rem;border-radius:.2rem;cursor:pointer;transition:background-color .1s ease-in-out,color .1s ease-in-out}.c16.revealed{background-color:transparent;color:inherit}.c17{display:flex;align-items:center;gap:.5rem;font-size:.8rem;color:#b5bac1;cursor:pointer;margin-left:2rem}.c18{flex-shrink:0;width:2.25rem;height:2.25rem;color:#b5bac1}.c19{width:1.75rem;height:1.75rem;border-radius:50%;flex-shrink:0}.c20{font-weight:600;color:#dbdee1;margin-right:.3rem}.c21{background-color:#5865f2;color:white;font-weight:600;font-size:70%;padding:.1rem .3rem;border-radius:.25rem;letter-spacing:.03rem;height:fit-content;align-self:center;flex-shrink:0}.c22{flex-grow:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#b5bac1;font-size:.75rem}.c23{background-color:#2b2d31;border:.15rem solid #2b2d31;border-left:.25rem solid;border-radius:.25rem;padding:.5rem .75rem;margin-top:.5rem;display:flex;flex-direction:column;gap:.5rem;max-width:40rem;min-width:30rem}.c23 a{color:#00aff4;text-decoration:none}.c23 a:hover{text-decoration:underline}.c24{display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem}.c25{display:flex;flex-direction:column;gap:.25rem}.c26{display:flex;align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;color:#fff;margin-bottom:.5rem}.c27{width:1.5rem;height:1.5rem;border-radius:50%}.c28{color:#fff;font-weight:500}.c29{font-size:1rem;font-weight:700;color:#fff;margin-bottom:.75rem}.c30{max-width:80px;max-height:80px;object-fit:contain;border-radius:.25rem;flex-shrink:0}.c31{font-size:.875rem;color:#dcddde}.c32{display:flex;flex-wrap:wrap;gap:.5rem}.c33{flex:1;min-width:150px}.c34{font-size:.75rem;font-weight:700;color:#fff;margin-bottom:.25rem}.c35{font-size:.875rem;color:#dcddde}.c36{margin-top:.5rem;max-width:100%;height:auto}.c36 img{max-width:100%;max-height:300px;object-fit:contain;border-radius:.25rem}.c37{display:flex;align-items:center;gap:.5rem;font-size:.75rem;color:#999;margin-top:.5rem}.c38{width:1.25rem;height:1.25rem;border-radius:50%}.c39{color:#999}.c40,.c41{max-width:400px;height:auto;border-radius:.25rem;margin-top:.5rem}.c42{width:300px;margin-top:.5rem}.c43{background-color:#2b2d31;border:1px solid #202225;border-radius:.75rem;padding:.75rem;display:flex;align-items:center;gap:.75rem;max-width:400px;margin-top:.5rem;width:fit-content}.c44{width:2.5rem;height:2.5rem;fill:#b9bbbe;flex-shrink:0}.c45{display:flex;flex-direction:column;gap:.1rem;overflow:hidden;flex-grow:1}.c46{color:#fff;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.c47{font-size:.75rem;color:#72767d}.c48{display:block;flex-shrink:0}.c49{width:1.5rem;height:1.5rem;fill:#b9bbbe;transition:fill .2s ease}.c48:hover .c49{fill:#fff}.c50{position:relative;display:inline-block;border-radius:.5rem;overflow:hidden;cursor:pointer}.c50 .c51{filter:blur(64px);pointer-events:none;transition:filter .2s ease;width:100%;height:100%}.c50 .c52{position:absolute;inset:0;background:rgba(32,34,37,.85);color:#fff;font-weight:600;letter-spacing:.05em;display:flex;align-items:center;justify-content:center;z-index:2;user-select:none}.c50.revealed .c51{filter:none;pointer-events:auto}.c50.revealed .c52{display:none}.c53{display:flex;gap:.5rem;margin-top:.5rem}.c54{display:flex;align-items:center;gap:.5rem;padding:0 .8rem;height:2.5rem;border-radius:.6rem;color:white;font-weight:600;cursor:pointer;transition:filter .2s ease}.c54:hover{filter:brightness(1.1)}.c55{font-size:1.25rem}.c56{font-size:.875rem}.c57{display:flex;align-items:center;gap:.5rem;color:white;font-weight:600}.c58{width:1.25rem;height:1.25rem}.c59{width:100%;position:relative}.c60{background-color:#2b2d31;border:1px solid #202225;border-radius:.75rem;padding:.75rem;min-width:17.5rem;cursor:pointer;user-select:none}.c61{color:#808080}.c62{display:none;position:absolute;top:100%;left:0;width:100%;background-color:#2b2d31;border:1px solid #202225;border-radius:1rem;margin-top:.25rem;padding:.5rem;z-index:10;box-sizing:border-box}.c59.active .c62{display:block}.c63{display:flex;align-items:center;gap:.5rem;padding:.75rem;border-radius:.75rem;cursor:pointer;transition:background-color .2s ease}.c63:hover{background-color:#4f545c}.c64{font-size:1.25rem}.c65{display:flex;flex-direction:column}.c66{font-weight:500}.c67{font-size:.75rem;color:#808080}.c68{display:flex;flex-wrap:wrap;gap:.25rem;width:100%;max-width:40rem;aspect-ratio:1/1;border:1px solid #202225;padding:.35rem;overflow:hidden}.c69{flex-grow:1;flex-basis:0;min-width:30%;display:flex}.c70{width:100%;height:100%;object-fit:cover;display:block;border-radius:1rem}.c71{background-color:#2b2d31;border-radius:.5rem;padding:1rem;max-width:40rem;min-width:30rem;border-left:.25rem solid}.c72{display:flex;flex-direction:row;justify-content:space-between;padding:.5rem 0}.c73{margin-right:.5rem;margin-left:1rem}.c74{width:5rem;height:5rem;border-radius:.5rem}.c75{padding:.5rem 0}.c76{border:1px solid #808080}.c77{background-color:#2b2d31;border-radius:.5rem;padding:1rem;margin-top:.5rem;max-width:40rem;min-width:25rem}.c78{font-size:1.1rem;font-weight:700;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.c79{display:flex;flex-direction:column;gap:.5rem}.c80{background-color:#3a3c42;border-radius:.3rem;padding:.75rem;cursor:pointer;border:1px solid transparent;transition:border-color .2s ease;position:relative;overflow:hidden}.c80:hover{border-color:#4d515a}.c81{position:absolute;top:0;left:0;height:100%;background-color:#5664fa7a;border-radius:.2rem;z-index:1}.c82{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between}.c83{display:flex;align-items:center;gap:.5rem}.c84{font-size:1.25rem}.c85{font-weight:500}.c86{font-size:.8rem;color:#b5bac1;font-weight:700}.c87{margin-top:1rem;font-size:.75rem;color:#949ba4}.c88{background-color:#2b2d31;border-radius:.5rem;padding:1rem;margin-top:.5rem;border:1px solid #3a3c42;min-width:20rem;max-width:40rem;display:flex;flex-direction:row;justify-content:space-between}.c89{display:flex;align-items:center;gap:.5rem;font-size:1rem;font-weight:600;margin-bottom:.4rem}.c90{color:#57f287;font-size:1.1em}.c91{font-size:.9rem;color:#b5bac1}.c92{margin-right:.5rem;margin-left:1rem;align-self:center}.c93{background-color:black;color:white;padding:.5rem 1rem;border-radius:.3rem;text-decoration:none;font-weight:500;transition:background-color .2s ease;cursor:pointer}.c93:hover{filter:brightness(1.1)}.c94{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.5rem}.c95{align-items:center;background-color:#2b2d31;border:1px solid #3a3c42;border-radius:1rem;padding:.25rem .6rem;font-size:1rem;color:#dcddde;font-weight:700;cursor:pointer}.c95:hover{filter:brightness(1.1)}`;
@@ -0,0 +1,20 @@
1
+ import { JsonData } from "../../types/types.js";
2
+ export declare class Html {
3
+ data: JsonData;
4
+ dateFormat: Intl.DateTimeFormat;
5
+ constructor(data: JsonData);
6
+ private getIcon;
7
+ private headerBuilder;
8
+ private messagesBuilder;
9
+ toHTML(): string;
10
+ private pollBuilder;
11
+ private pollResultEmbedBuilder;
12
+ private embedBuilder;
13
+ private attachmentBuilder;
14
+ private componentBuilder;
15
+ private buttonBuilder;
16
+ private selectorBuilder;
17
+ private reactionsBuilder;
18
+ private spoilerAttachmentBuilder;
19
+ private svgBuilder;
20
+ }
@@ -0,0 +1,441 @@
1
+ import { CustomError } from "../../core/error.js";
2
+ import { markdownToHTML } from "../../core/markdown.js";
3
+ import { JsonButtonStyle, JsonComponentType } from "../../types/types.js";
4
+ import { ACTIONROW_CSS, ATTACHMENT_CSS, BUTTON_CSS, COMPONENTS_CSS, COMPONENTSV2_CSS, DEFAULT_CSS, EMBED_CSS, MESSAGE_CSS, POLL_CSS, POLL_RESULT_EMBED_CSS, REACTIONS_CSS } from "./css.js";
5
+ import { script } from "./js.js";
6
+ import packageJson from "./../../../package.json" with { type: 'json' };
7
+ const COUNT_UNIT = ["KB", "MB", "GB", "TB"];
8
+ const BUTTON_COLOR = ["black", "#5865f2", "#323538", "#32c05f", "#be3638", "#323538", "#5865f2"];
9
+ export class Html {
10
+ data;
11
+ dateFormat;
12
+ constructor(data) {
13
+ this.data = data;
14
+ try {
15
+ this.dateFormat = new Intl.DateTimeFormat(data.options.localDate, {
16
+ timeZone: data.options.timeZone,
17
+ day: '2-digit',
18
+ month: '2-digit',
19
+ year: 'numeric',
20
+ hour: '2-digit',
21
+ minute: '2-digit',
22
+ second: '2-digit'
23
+ });
24
+ }
25
+ catch (error) {
26
+ throw new CustomError("[discord-message-transcript] Invalid LocalDate and/or TimeZone.");
27
+ }
28
+ }
29
+ getIcon() {
30
+ const { guild, channel } = this.data;
31
+ if (guild) {
32
+ if (guild.icon) {
33
+ return `<img src="${guild.icon}" style="width: 7rem; height: 7rem; border-radius: 50%;">`;
34
+ }
35
+ else {
36
+ return `<div class="guildInitialsIcon">${initials(guild.name)}</div>`;
37
+ }
38
+ }
39
+ else {
40
+ return `<img src="${channel.img}" style="width: 7rem; height: 7rem; border-radius: 50%;">`;
41
+ }
42
+ function initials(name) {
43
+ const words = name.split(' ').filter(word => word.length > 0);
44
+ if (words.length >= 1) {
45
+ return words.map(word => word[0]).join('').substring(0, 3).toUpperCase();
46
+ }
47
+ return name.substring(0, 1).toUpperCase();
48
+ }
49
+ }
50
+ headerBuilder() {
51
+ const { channel, guild } = this.data;
52
+ return `
53
+ <div style="display: flex; gap: 1.5rem; align-items: center; width 100vw">
54
+ ${this.getIcon()}
55
+ <div style="display: flex; flex-direction: column; justify-content: center; gap: 1.25rem;">
56
+ ${guild ? `<div id="guild" class="line">
57
+ <h4>Guild: </h4>
58
+ <h4 style="font-weight: normal;">${guild.name}</h4>
59
+ </div>` : ""}
60
+ ${channel.parent ? `<div id="category" class="line">
61
+ <h4>Category: </h4>
62
+ <h4 style="font-weight: normal;">${channel.parent.name}</h4>
63
+ </div>` : ""}
64
+ <div id="channel" class="line">
65
+ <h4>Channel: </h4>
66
+ <h4 style="font-weight: normal;">${channel.name}</h4>
67
+ </div>
68
+ ${channel.topic ? `<div id="topic" class="line">
69
+ <h4>Topic: </h4>
70
+ <h4 style="font-weight: normal;">${channel.topic}</h4>
71
+ </div>` : ""}
72
+ </div>
73
+ </div>
74
+ `;
75
+ }
76
+ messagesBuilder() {
77
+ return this.data.messages.map(message => {
78
+ const date = new Date(message.createdTimestamp);
79
+ return `
80
+ <div class="messageDiv" id="${message.id}" data-author-id="${message.authorId}">
81
+ ${message.references && message.references.messageId ?
82
+ `<div class="messageReply" data-id="${message.references.messageId}">
83
+ <svg class="messageReplySvg"><use href="#reply-icon"></use></svg>
84
+ <img class="messageReplyImg" src="">
85
+ <div class="replyBadges"></div>
86
+ <div class="messageReplyText"></div>
87
+ </div>` : ""}
88
+ <div class="messageBotton">
89
+ <img src="" class="messageImg">
90
+ <div class="messageDivRight">
91
+ <div class="messageUser">
92
+ <h3 class="messageUsername"></h3>
93
+ <div class="badges"></div>
94
+ <p class="messageTimeStamp">${this.dateFormat.format(date)}</p>
95
+ </div>
96
+ <div class="messageContent">${markdownToHTML(message.content, this.data.mentions, message.mentions, this.dateFormat)}</div>
97
+ ${message.poll ? this.pollBuilder(message.poll) : ""}
98
+ ${message.embeds.length > 0 ? this.embedBuilder(message, message.embeds) : ""}
99
+ ${message.attachments.length > 0 ? this.attachmentBuilder(message.attachments) : ""}
100
+ ${message.components.length > 0 ? this.componentBuilder(message, message.components) : ""}
101
+ ${message.reactions.length > 0 ? this.reactionsBuilder(message.reactions) : ""}
102
+ </div>
103
+ </div>
104
+ </div>
105
+ `;
106
+ }).join("");
107
+ }
108
+ toHTML() {
109
+ const { options } = this.data;
110
+ const cssContent = `
111
+ ${DEFAULT_CSS}
112
+ ${MESSAGE_CSS}
113
+ ${options.includePolls ? POLL_CSS + POLL_RESULT_EMBED_CSS : ""}
114
+ ${options.includeEmbeds ? EMBED_CSS : ""}
115
+ ${options.includeButtons || options.includeComponents ? ACTIONROW_CSS : ""}
116
+ ${options.includeAttachments || options.includeV2Components ? ATTACHMENT_CSS : ""}
117
+ ${options.includeButtons || options.includeV2Components ? BUTTON_CSS : ""}
118
+ ${options.includeComponents ? COMPONENTS_CSS : ""}
119
+ ${options.includeV2Components ? COMPONENTSV2_CSS : ""}
120
+ ${options.includeReactions ? REACTIONS_CSS : ""}
121
+ `;
122
+ const jsContent = script(options.includeComponents, options.includePolls);
123
+ return `
124
+ <!DOCTYPE html>
125
+ <html lang="${options.localDate}">
126
+ <head>
127
+ <meta charset="UTF-8" />
128
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
129
+ <title>${options.fileName}</title>
130
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/atom-one-dark.min.css">
131
+ <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
132
+ ${options.selfContained ? `<style>${cssContent}</style>` : `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/discord-message-transcript-base@${packageJson.version}/dist/assets/style.css">`}
133
+ </head>
134
+ <body>
135
+ ${this.svgBuilder()}
136
+ <header>
137
+ ${this.headerBuilder()}
138
+ </header>
139
+ <main style="display: flex; flex-direction: column; padding: 2.25%;">
140
+ ${this.messagesBuilder()}
141
+ </main>
142
+ ${options.watermark ? `<footer>
143
+ <br>
144
+ <div style="padding: 1rem 0; font-weight: 700; text-align: center; font-size: 1.5rem; background-color: #2b2d31;">Transcript generated by <a href="https://github.com/HenriqueMairesse/discord-message-transcript">discord-message-transcript</a></div>
145
+ </footer> ` : ""}
146
+ <script id="authorData" type="application/json">
147
+ ${JSON.stringify({ authors: this.data.authors })}
148
+ </script>
149
+ ${options.selfContained ? `<script>${jsContent}</script>` : `<script src="https://cdn.jsdelivr.net/npm/discord-message-transcript-base@${packageJson.version}/dist/assets/script.js"></script>`}
150
+ </body>
151
+ </html>
152
+ `;
153
+ }
154
+ pollBuilder(poll) {
155
+ const totalVotes = poll.answers.reduce((acc, answer) => acc + answer.count, 0);
156
+ let footerText = `${totalVotes} votes`;
157
+ if (poll.isFinalized) {
158
+ footerText += ` • Poll closed`;
159
+ }
160
+ else if (poll.expiry) {
161
+ footerText += ` • ${poll.expiry}`;
162
+ }
163
+ return `
164
+ <div class="pollDiv">
165
+ <div class="pollQuestion">${poll.question}</div>
166
+ <div class="pollAnswers">
167
+ ${poll.answers.map(answer => {
168
+ const voteCount = answer.count;
169
+ const percentage = totalVotes > 0 ? (voteCount / totalVotes) * 100 : 0;
170
+ return `
171
+ <div class="pollAnswer">
172
+ <div class="pollAnswerBar" style="width: ${percentage}%;"></div>
173
+ <div class="pollAnswerContent">
174
+ <div class="pollAnswerDetails">
175
+ ${answer.emoji ? `<div class="pollAnswerEmoji">${answer.emoji.name}</div>` : ''}
176
+ <div class="pollAnswerText">${answer.text}</div>
177
+ </div>
178
+ <div class="pollAnswerVotes">${voteCount}</div>
179
+ </div>
180
+ </div>
181
+ `;
182
+ }).join('')}
183
+ </div>
184
+ <div class="pollFooter">${footerText}</div>
185
+ </div>
186
+ `;
187
+ }
188
+ pollResultEmbedBuilder(embed, message) {
189
+ const getField = (name) => embed.fields?.find(f => f.name === name)?.value;
190
+ const winnerText = getField("victor_answer_text");
191
+ const emojiText = getField("victor_answer_emoji_name");
192
+ const winnerVotes = parseInt(getField("victor_answer_votes") ?? "0");
193
+ const totalVotes = parseInt(getField("total_votes") ?? "0");
194
+ const winnerPercentage = totalVotes > 0 ? (winnerVotes / totalVotes) * 100 : 0;
195
+ if (!winnerText && winnerVotes != 0)
196
+ return '';
197
+ return `
198
+ <div class="pollResultEmbed">
199
+ <div>
200
+ <div class="pollResultEmbedWinner">
201
+ ${emojiText ? emojiText : ""}
202
+ ${winnerText ?? "There was no winner"}
203
+ ${winnerVotes != 0 ? `<span class="pollResultEmbedCheckmark">✔</span>` : ""}
204
+ </div>
205
+ <div class="pollResultEmbedSubtitle">${totalVotes} votes (${winnerPercentage.toFixed(1)}%)</div>
206
+ </div>
207
+ <div class="pollResultEmbedButtonDiv">
208
+ <div data-id="${message.references?.messageId ?? ""}" class="pollResultEmbedButton">View Poll</div>
209
+ </div>
210
+ </div>
211
+ `;
212
+ }
213
+ embedBuilder(message, embeds) {
214
+ return embeds.map(embed => {
215
+ if (embed.type === 'poll_result')
216
+ return this.pollResultEmbedBuilder(embed, message);
217
+ if (!this.data.options.includeEmbeds)
218
+ return "";
219
+ const embedAuthor = embed.author ? (embed.author.url ? `<a class="embedHeaderLefttAuthorName" href="${embed.author.url}" target="_blank">${embed.author.name}</a>` : `<p class="embedHeaderLeftAuthorName">${embed.author.name}</p>`) : "";
220
+ const embedTitle = embed.title ? (embed.url ? `<a class="embedHeaderLeftTitle" href="${embed.url}" target="_blank">${embed.title}</a>` : `<p class="embedHeaderLeftTitle">${embed.title}</p>`) : "";
221
+ return `
222
+ <div class="embed" style="${embed.hexColor ? `border-left-color: ${embed.hexColor}` : ''}">
223
+ ${embed.author || embed.title || embed.thumbnail || embed.description ? `
224
+ <div class="embedHeader">
225
+ <div class="embedHeaderLeft">
226
+ ${embed.author ? `
227
+ <div class="embedHeaderLeftAuthor">
228
+ ${embed.author.iconURL ? `<img class="embedHeaderLeftAuthorImg" src="${embed.author.iconURL}">` : ""}
229
+ ${embedAuthor}
230
+ </div>` : ""}
231
+ ${embedTitle}
232
+ ${embed.description ? `<div class="embedDescription">${markdownToHTML(embed.description, this.data.mentions, message.mentions, this.dateFormat)}</div>` : ""}
233
+ </div>
234
+ ${embed.thumbnail ? `<img class="embedHeaderThumbnail" src="${embed.thumbnail.url}">` : ""}
235
+ </div>` : ""}
236
+ ${embed.fields && embed.fields.length > 0 ? `
237
+ <div class="embedFields">
238
+ ${embed.fields.map(field => `
239
+ <div class="embedFieldsField" style="${field.inline ? 'display: inline-block;' : ''}">
240
+ <p class="embedFieldsFieldTitle">${field.name}</p>
241
+ <p class="embedFieldsFieldValue">${markdownToHTML(field.value, this.data.mentions, message.mentions, this.dateFormat)}</p>
242
+ </div>`).join("")}
243
+ </div>` : ""}
244
+ ${embed.image ? `
245
+ <div class="embedImage">
246
+ <img src="${embed.image.url}">
247
+ </div>` : ""}
248
+ ${embed.footer || embed.timestamp ? `
249
+ <div class="embedFooter">
250
+ ${embed.footer?.iconURL ? `<img class="embedFooterImg" src="${embed.footer.iconURL}">` : ""}
251
+ ${embed.footer?.text || embed.timestamp ? `<p class="embedFooterText">${embed.footer?.text ?? ''}${embed.footer?.text && embed.timestamp ? ' | ' : ''}${embed.timestamp ? this.dateFormat.format(new Date(embed.timestamp)) : ''}</p>` : ""}
252
+ </div>` : ""}
253
+ </div>
254
+ `;
255
+ }).join("");
256
+ }
257
+ attachmentBuilder(attachments) {
258
+ return attachments.map(attachment => {
259
+ let html = "";
260
+ if (attachment.contentType?.startsWith('image/')) {
261
+ html = `<img class="attachmentImage" src="${attachment.url}">`;
262
+ }
263
+ else if (attachment.contentType?.startsWith('video/')) {
264
+ html = `<video class="attachmentVideo" controls src="${attachment.url}"></video>`;
265
+ }
266
+ else if (attachment.contentType?.startsWith('audio/')) {
267
+ html = `<audio class="attachmentAudio" controls src="${attachment.url}"></audio>`;
268
+ }
269
+ else {
270
+ let fileSize = attachment.size / 1024;
271
+ let count = 0;
272
+ while (fileSize > 512 && count < COUNT_UNIT.length - 1) {
273
+ fileSize = fileSize / 1024;
274
+ count++;
275
+ }
276
+ html = `
277
+ <div class="attachmentFile">
278
+ <div class="attachmentFileInfo">
279
+ <p class="attachmentFileName">${attachment.name ?? 'attachment'}</p>
280
+ <div class="attachmentFileSize">${fileSize.toFixed(2)} ${COUNT_UNIT[count]}</div>
281
+ </div>
282
+ <a class="attachmentDownload" href="${attachment.url}" target="_blank">
283
+ <svg class="attachmentDownloadIcon"><use href="#download-icon"></use></svg>
284
+ </a>
285
+ </div>
286
+ `;
287
+ }
288
+ return this.spoilerAttachmentBuilder(attachment.spoiler, html);
289
+ }).join("");
290
+ }
291
+ componentBuilder(message, components) {
292
+ return components.map(component => {
293
+ switch (component.type) {
294
+ case JsonComponentType.ActionRow: {
295
+ if (!component.components[0])
296
+ return "";
297
+ return `
298
+ <div class="actionRow">
299
+ ${component.components[0].type == JsonComponentType.Button ? component.components.map(button => {
300
+ return button.type == JsonComponentType.Button ? this.buttonBuilder(button) : "";
301
+ }).join("") : this.selectorBuilder(component.components[0])}
302
+ </div>
303
+ `;
304
+ }
305
+ case JsonComponentType.Container: {
306
+ const html = `
307
+ <div class="container" style="${component.hexAccentColor ? `border-left-color: ${component.hexAccentColor}` : ''}">
308
+ ${this.componentBuilder(message, component.components)}
309
+ </div>
310
+ `;
311
+ return this.spoilerAttachmentBuilder(component.spoiler, html);
312
+ }
313
+ case JsonComponentType.File: {
314
+ let fileSize = (component.size ?? 0) / 1024;
315
+ let count = 0;
316
+ while (fileSize > 512 && count < COUNT_UNIT.length - 1) {
317
+ fileSize = fileSize / 1024;
318
+ count++;
319
+ }
320
+ const html = `
321
+ <div class="attachmentFile">
322
+ <div class="attachmentFileInfo">
323
+ <p class="attachmentFileName">${component.fileName ?? 'file'}</p>
324
+ <div class="attachmentFileSize">${fileSize.toFixed(2)} ${COUNT_UNIT[count]}</div>
325
+ </div>
326
+ <a class="attachmentDownload" href="${component.url ?? ''}" target="_blank">
327
+ <svg class="attachmentDownloadIcon"><use href="#download-icon"></use></svg>
328
+ </a>
329
+ </div>
330
+ `;
331
+ return this.spoilerAttachmentBuilder(component.spoiler, html);
332
+ }
333
+ case JsonComponentType.MediaGallery: {
334
+ return `
335
+ <div class="mediaGallery">
336
+ ${component.items.map(image => {
337
+ return `
338
+ <div class="mediaGalleryItem">
339
+ ${this.spoilerAttachmentBuilder(image.spoiler, `<img class="mediaGalleryImg" src="${image.media.url}">`)}
340
+ </div>
341
+ `;
342
+ }).join("")}
343
+ </div>
344
+ `;
345
+ }
346
+ case JsonComponentType.Section: {
347
+ return `
348
+ <div class="section">
349
+ <div class="sectionLeft">
350
+ ${this.componentBuilder(message, component.components)}
351
+ </div>
352
+ <div class="sectionRight">
353
+ ${component.accessory.type == JsonComponentType.Button ? this.buttonBuilder(component.accessory)
354
+ : component.accessory.type == JsonComponentType.Thumbnail ? this.spoilerAttachmentBuilder(component.accessory.spoiler, `
355
+ <img class="sectionThumbnail" src="${component.accessory.media.url}">
356
+ `) : ""}
357
+ </div>
358
+ </div>
359
+ `;
360
+ }
361
+ case JsonComponentType.Separator: {
362
+ return `<hr class="separator" style="${component.divider ? "" : "visibility: hidden;"} ${component.spacing == 1 ? "margin: 0.15rem 0;" : "margin: 0.3rem 0;"}">`;
363
+ }
364
+ case JsonComponentType.TextDisplay: {
365
+ return `<div class="textDisplay">${markdownToHTML(component.content, this.data.mentions, message.mentions, this.dateFormat)}</div>`;
366
+ }
367
+ default:
368
+ return ``;
369
+ }
370
+ }).join("");
371
+ }
372
+ buttonBuilder(button) {
373
+ return `
374
+ <div class="button" style="background-color: ${BUTTON_COLOR[button.style]}">
375
+ ${button.style == JsonButtonStyle.Link && button.url ? `
376
+ <a class="buttonLink" href="${button.url}" target="_blank">
377
+ ${button.emoji ? `<p class="buttonEmoji">${button.emoji}</p>` : ""}
378
+ ${button.label ? `<p class="buttonLabel">${button.label}</p>` : ""}
379
+ <svg class="buttonLinkIcon"><use href="#link-icon"></use></svg>
380
+ </a>` : `
381
+ ${button.emoji ? `<p class="buttonEmoji">${button.emoji}</p>` : ""}
382
+ ${button.label ? `<p class="buttonLabel">${button.label}</p>` : ""}
383
+ `}
384
+ </div>
385
+ `;
386
+ }
387
+ selectorBuilder(selector) {
388
+ return `
389
+ <div class="selector">
390
+ <div class="selectorInput">
391
+ <p class="selectorInputText">${selector.placeholder}</p>
392
+ </div>
393
+ <div class="selectorOptionMenu">
394
+ ${selector.type == JsonComponentType.StringSelect ? selector.options.map(option => {
395
+ return `
396
+ <div class="selectorOption">
397
+ ${option.emoji ? `<p class="selectorOptionEmoji">${option.emoji}</p>` : ""}
398
+ <div class="selectorOptionRight">
399
+ <p class="selectorOptionTitle">${option.label}</p>
400
+ ${option.description ? `<p class="selectorOptionDesc">${option.description}</p>` : ""}
401
+ </div>
402
+ </div>
403
+ `;
404
+ }).join("") : ""}
405
+ </div>
406
+ </div>
407
+ `;
408
+ }
409
+ reactionsBuilder(reactions) {
410
+ return `
411
+ <div class="reactionsDiv">
412
+ ${reactions.map(reaction => `
413
+ <div class="reaction"><p>${reaction.count} ${reaction.emoji}</p></div>
414
+ `).join('')}
415
+ </div>
416
+ `;
417
+ }
418
+ spoilerAttachmentBuilder(spoiler, html) {
419
+ return spoiler ? `<div class="spoilerAttachment"><div class="spoilerAttachmentOverlay">SPOILER</div><div class="spoilerAttachmentContent">${html}</div></div>` : html;
420
+ }
421
+ svgBuilder() {
422
+ const { options } = this.data;
423
+ return `
424
+ <svg style="display: none;">
425
+ <defs>
426
+ <symbol id="reply-icon" viewBox="0 0 16 16" fill="none">
427
+ <g transform="rotate(90 8 8)">
428
+ <path d="M6 2V9C6 11.5 8.5 14 11 14H14" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
429
+ </g>
430
+ </symbol>
431
+ ${options.includeAttachments ? `<symbol id="download-icon" viewBox="0 -960 960 960">
432
+ <path d="m720-120 160-160-56-56-64 64v-167h-80v167l-64-64-56 56 160 160ZM560 0v-80h320V0H560ZM240-160q-33 0-56.5-23.5T160-240v-560q0-33 23.5-56.5T240-880h280l240 240v121h-80v-81H480v-200H240v560h240v80H240Zm0-80v-560 560Z"/>
433
+ </symbol> ` : ""}
434
+ ${options.includeButtons ? `<symbol id="link-icon" viewBox="0 -960 960 960" fill="#e3e3e3">
435
+ <path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/>
436
+ </symbol>` : ""}
437
+ </defs>
438
+ </svg>
439
+ `;
440
+ }
441
+ }
@@ -0,0 +1,3 @@
1
+ export declare function script(includeComponents: boolean, includePolls: boolean): string;
2
+ export declare const POLL_JS = "\nconst pollDiv = event.target.closest('.pollResultEmbedButton');\nif (pollDiv) {\n event.preventDefault();\n const messageId = pollDiv.dataset.id;\n if (!messageId || messageId == \"\") return;\n const message = document.getElementById(messageId);\n\n if (message) {\n message.scrollIntoView({ behavior: 'smooth', block: 'center' });\n message.classList.add('highlight');\n setTimeout(() => {\n message.classList.remove('highlight');\n }, 1500);\n }\n}\n";
3
+ export declare const SELECTOR_JS = "\nconst selectorInput = event.target.closest('.selectorInput');\ndocument.querySelectorAll('.selector').forEach(selector => {\n if (!selector.contains(event.target)) {\n selector.classList.remove('active');\n }\n});\n\nif (selectorInput) {\n const selector = selectorInput.closest('.selector');\n if (selector) {\n selector.classList.toggle('active');\n }\n}\n";
@@ -0,0 +1,174 @@
1
+ export function script(includeComponents, includePolls) {
2
+ return `
3
+ document.addEventListener('DOMContentLoaded', () => {
4
+ const transcriptDataElement = document.getElementById('authorData');
5
+ if (!transcriptDataElement) {
6
+ console.error('Missing author data element.');
7
+ return;
8
+ }
9
+
10
+ const data = JSON.parse(transcriptDataElement.textContent);
11
+ const authorMap = new Map((data.authors || []).map(author => [author.id, author]));
12
+
13
+ document.querySelectorAll('.messageDiv[data-author-id]').forEach(messageDiv => {
14
+ const authorId = messageDiv.dataset.authorId;
15
+ const author = authorMap.get(authorId);
16
+
17
+ if (!author) return;
18
+
19
+ const avatarImg = messageDiv.querySelector('.messageImg');
20
+ if (avatarImg) avatarImg.src = author.avatarURL;
21
+
22
+ const username = messageDiv.querySelector('.messageUsername');
23
+ if (username) {
24
+ username.textContent = author.member?.displayName ?? author.displayName;
25
+ username.style.color = author.member?.displayHexColor ?? '#dbdee1';
26
+ }
27
+
28
+ const badgesDiv = messageDiv.querySelector('.badges');
29
+ if (badgesDiv) {
30
+ badgesDiv.innerHTML = '';
31
+ if (author.bot) {
32
+ const badge = document.createElement('p');
33
+ badge.className = 'badge';
34
+ badge.textContent = 'APP';
35
+ badgesDiv.appendChild(badge);
36
+ }
37
+ if (author.system) {
38
+ const badge = document.createElement('p');
39
+ badge.className = 'badge';
40
+ badge.textContent = 'SYSTEM';
41
+ badgesDiv.appendChild(badge);
42
+ }
43
+ if (author.guildTag) {
44
+ const badge = document.createElement('p');
45
+ badge.className = 'badgeTag';
46
+ badge.textContent = author.guildTag;
47
+ badgesDiv.appendChild(badge);
48
+ }
49
+ }
50
+ });
51
+
52
+ document.querySelectorAll('.messageReply[data-id]').forEach(replyDiv => {
53
+ const repliedToId = replyDiv.dataset.id;
54
+ if (!repliedToId) return;
55
+
56
+ const repliedToMessageDiv = document.getElementById(repliedToId);
57
+ if (!repliedToMessageDiv) return;
58
+
59
+ const repliedToAuthorId = repliedToMessageDiv.dataset.authorId;
60
+ const repliedToAuthor = authorMap.get(repliedToAuthorId);
61
+
62
+ if (repliedToAuthor) {
63
+ const replyImg = replyDiv.querySelector('.messageReplyImg');
64
+ if (replyImg) replyImg.src = repliedToAuthor.avatarURL;
65
+
66
+ const replyBadgesDiv = replyDiv.querySelector('.replyBadges');
67
+ if (replyBadgesDiv) {
68
+ replyBadgesDiv.innerHTML = '';
69
+ if (repliedToAuthor.bot) {
70
+ const badge = document.createElement('p');
71
+ badge.className = 'badge';
72
+ badge.textContent = 'APP';
73
+ replyBadgesDiv.appendChild(badge);
74
+ }
75
+ if (repliedToAuthor.system) {
76
+ const badge = document.createElement('p');
77
+ badge.className = 'badge';
78
+ badge.textContent = 'SYSTEM';
79
+ replyBadgesDiv.appendChild(badge);
80
+ }
81
+ if (repliedToAuthor.guildTag) {
82
+ const badge = document.createElement('p');
83
+ badge.className = 'badgeTag';
84
+ badge.textContent = repliedToAuthor.guildTag;
85
+ replyBadgesDiv.appendChild(badge);
86
+ }
87
+ }
88
+ }
89
+
90
+ const replyTextDiv = replyDiv.querySelector('.messageReplyText');
91
+ if (replyTextDiv) {
92
+ const originalContent = repliedToMessageDiv.querySelector('.messageContent');
93
+ if (originalContent) {
94
+ let content = originalContent.textContent || '';
95
+ if (content.length > 100) {
96
+ content = content.substring(0, 100).trim() + '...';
97
+ }
98
+
99
+ const authorName = repliedToAuthor?.member?.displayName ?? repliedToAuthor?.displayName ?? 'Unknown';
100
+ const authorColor = repliedToAuthor?.member?.displayHexColor ?? 'inherit';
101
+
102
+ const authorNameSpan = \`<span style="color: \${authorColor};">\${authorName}</span>\`;
103
+ replyTextDiv.innerHTML = authorNameSpan + " " + content;
104
+ }
105
+ }
106
+ });
107
+
108
+ document.addEventListener('click', function (event) {
109
+ const spoiler = event.target.closest('.spoilerMsg, .spoilerAttachment');
110
+ if (spoiler && !spoiler.classList.contains('revealed')) {
111
+ event.preventDefault();
112
+ event.stopPropagation();
113
+ spoiler.classList.add('revealed');
114
+ }
115
+
116
+ const replyDiv = event.target.closest('.messageReply');
117
+ if (replyDiv) {
118
+ event.preventDefault();
119
+ const messageId = replyDiv.dataset.id;
120
+ if (!messageId) return;
121
+
122
+ const message = document.getElementById(messageId);
123
+ if (message) {
124
+ message.scrollIntoView({ behavior: 'smooth', block: 'center' });
125
+ message.classList.add('highlight');
126
+ setTimeout(() => {
127
+ message.classList.remove('highlight');
128
+ }, 1500);
129
+ }
130
+ }
131
+
132
+ ${includeComponents ? SELECTOR_JS : ""}
133
+
134
+ ${includePolls ? POLL_JS : ""}
135
+ });
136
+
137
+ if (window.hljs) {
138
+ hljs.highlightAll();
139
+ }
140
+ });
141
+ `;
142
+ }
143
+ export const POLL_JS = `
144
+ const pollDiv = event.target.closest('.pollResultEmbedButton');
145
+ if (pollDiv) {
146
+ event.preventDefault();
147
+ const messageId = pollDiv.dataset.id;
148
+ if (!messageId || messageId == "") return;
149
+ const message = document.getElementById(messageId);
150
+
151
+ if (message) {
152
+ message.scrollIntoView({ behavior: 'smooth', block: 'center' });
153
+ message.classList.add('highlight');
154
+ setTimeout(() => {
155
+ message.classList.remove('highlight');
156
+ }, 1500);
157
+ }
158
+ }
159
+ `;
160
+ export const SELECTOR_JS = `
161
+ const selectorInput = event.target.closest('.selectorInput');
162
+ document.querySelectorAll('.selector').forEach(selector => {
163
+ if (!selector.contains(event.target)) {
164
+ selector.classList.remove('active');
165
+ }
166
+ });
167
+
168
+ if (selectorInput) {
169
+ const selector = selectorInput.closest('.selector');
170
+ if (selector) {
171
+ selector.classList.toggle('active');
172
+ }
173
+ }
174
+ `;