discord-html-transcripts-fix 2.3.0 → 2.3.1
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.
- package/README.md +3 -0
- package/dist/generator/renderers/components/DiscordHighlightedCode.js +30 -4
- package/dist/generator/renderers/components/TranscriptHeader.js +22 -2
- package/dist/generator/renderers/content.js +44 -9
- package/dist/generator/renderers/message.js +3 -3
- package/dist/generator/renderers/systemMessage.js +1 -1
- package/dist/utils/utils.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -275,6 +275,9 @@ In addition to plain text, replies, embeds, and attachments, the viewer supports
|
|
|
275
275
|
- **Fix** link previews, GIFs (`gifv`) and video embeds get their own layout — discord.js keeps the embed type only in `embed.data`, so every embed rendered as `rich` and an image preview showed up as an empty card with a small thumbnail
|
|
276
276
|
- **Fix** super reactions and voice-message durations show — the renderer read the raw API names (`count_details`, `burst_colors`, `duration_secs`), which discord.js renames
|
|
277
277
|
- **Fix** reply authors get their role color, and embed footer icons and audio file sizes show — the values were passed as camelCase attributes, which HTML lowercases, so the components never received them. Members without a colored role are no longer painted black
|
|
278
|
+
- **Fix** text outside the Basic Multilingual Plane showed as pairs of `�` — fancy-font ("math italic") server and channel names, rare ideographs, symbols — in messages, embeds and Components V2 containers. The markdown parser cuts its text at every non-ASCII character and counts UTF-16 code units, so such a character arrived as two lone halves that could not be encoded. Text is merged back (and renders as one piece per run instead of one node per non-ASCII character, which also shrinks the HTML). Reply and thread previews, the search text, activity party ids and the server's initial letters no longer cut through a character either, and a server name with a double space no longer reads "Aundefined"
|
|
279
|
+
- **Fix** a fenced code block in a language highlight.js does not know — ` ```ansi `, ` ```log `, a typo such as `js2` — made the whole export fail, because the error was raised while rendering, outside the per-message safety net. Such blocks now show as plain text, like in Discord; `ansi` blocks lose their color escape codes
|
|
280
|
+
- **Fix** a role-subscription tier name containing `$&`, `$'`, `` $` `` or `$$` was altered when the message was composed; it is shown exactly as written
|
|
278
281
|
- **Fix** reaction counts of 1,000 and more showed `NaN` — the abbreviated "1.5K" went to a component that only reads numbers; the full count is shown now
|
|
279
282
|
- **Fix** system messages of types without their own wording — Server Discovery notices, invite reminders, premium upsells, purchase notifications, and any type Discord adds later — were silently dropped from the transcript. They appear as a neutral "System message" line with Discord's text now (new i18n key `systemMessage`)
|
|
280
283
|
- **Fix** the stats footer counts one of something in the singular — "1 message · 1 participant · 1 image" instead of "1 messages", in German too (new i18n keys `statsMessage`, `statsParticipant`, `statsImage`)
|
|
@@ -7,6 +7,7 @@ exports.DiscordHighlightStyles = void 0;
|
|
|
7
7
|
exports.DiscordHighlightedCode = DiscordHighlightedCode;
|
|
8
8
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
9
9
|
const highlight_js_1 = __importDefault(require("highlight.js"));
|
|
10
|
+
const utils_1 = require("../../../utils/utils");
|
|
10
11
|
// styles from https://github.com/cherryblossom000/discord-syntax-highlighting/tree/main
|
|
11
12
|
exports.DiscordHighlightStyles = `
|
|
12
13
|
.hljs-ansi-control-sequence {
|
|
@@ -190,10 +191,35 @@ exports.DiscordHighlightStyles = `
|
|
|
190
191
|
background-color: #67060c;
|
|
191
192
|
}
|
|
192
193
|
`;
|
|
194
|
+
const ESC = String.fromCharCode(27);
|
|
195
|
+
// Discord colors ANSI blocks; the transcript shows their text without the control sequences.
|
|
196
|
+
const ANSI_SGR = new RegExp(ESC + '\\[[0-9;]*m', 'g');
|
|
197
|
+
|
|
198
|
+
function escapeHtml(text) {
|
|
199
|
+
return String(text)
|
|
200
|
+
.replace(/&/g, '&')
|
|
201
|
+
.replace(/</g, '<')
|
|
202
|
+
.replace(/>/g, '>')
|
|
203
|
+
.replace(/"/g, '"')
|
|
204
|
+
.replace(/'/g, ''');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The block is rendered while React runs, outside every per-message safety net, so a
|
|
208
|
+
// throw here used to reject the whole export: highlight.js raises an error for a language
|
|
209
|
+
// it does not know ("ansi", "log", a typo such as "js2"), and users write those all the time.
|
|
210
|
+
// Those blocks show as plain text, which is what Discord does for them.
|
|
211
|
+
function highlightCode(content, language) {
|
|
212
|
+
const name = typeof language === 'string' ? language.trim() : '';
|
|
213
|
+
try {
|
|
214
|
+
if (!name) return highlight_js_1.default.highlightAuto(content).value;
|
|
215
|
+
if (highlight_js_1.default.getLanguage(name)) return highlight_js_1.default.highlight(content, { language: name }).value;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.warn('[discord-html-transcripts-fix] could not highlight a code block:', (0, utils_1.describeError)(err));
|
|
218
|
+
}
|
|
219
|
+
return escapeHtml(name.toLowerCase() === 'ansi' ? content.replace(ANSI_SGR, '') : content);
|
|
220
|
+
}
|
|
221
|
+
|
|
193
222
|
function DiscordHighlightedCode(props) {
|
|
194
|
-
|
|
195
|
-
? highlight_js_1.default.highlight(props.content, { language: props.language })
|
|
196
|
-
: highlight_js_1.default.highlightAuto(props.content);
|
|
197
|
-
return (0, jsx_runtime_1.jsx)("discord-code", { multiline: true, className: "theme-dark hljs", dangerouslySetInnerHTML: { __html: highlighted.value } });
|
|
223
|
+
return (0, jsx_runtime_1.jsx)("discord-code", { multiline: true, className: "theme-dark hljs", dangerouslySetInnerHTML: { __html: highlightCode(props.content, props.language) } });
|
|
198
224
|
}
|
|
199
225
|
//# sourceMappingURL=DiscordHighlightedCode.js.map
|
|
@@ -2,12 +2,32 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TranscriptHeader = TranscriptHeader;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
|
|
6
|
+
// The first user-perceived character of a word. word[0] is one UTF-16 code unit —
|
|
7
|
+
// half of a character outside the Basic Multilingual Plane (fancy-font letters, emoji),
|
|
8
|
+
// which cannot be encoded — and would also cut an accent off its letter or an emoji
|
|
9
|
+
// sequence apart.
|
|
10
|
+
const segmenter = typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
|
|
11
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
12
|
+
: null;
|
|
13
|
+
function firstCharacter(word) {
|
|
14
|
+
if (segmenter) {
|
|
15
|
+
for (const { segment } of segmenter.segment(word)) return segment;
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
const codePoint = word.codePointAt(0);
|
|
19
|
+
return codePoint === undefined ? '' : String.fromCodePoint(codePoint);
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
function TranscriptHeader(props) {
|
|
6
23
|
// If guild has no icon, we take first letter of guild name words
|
|
7
24
|
// i.e. Guild A -> GA
|
|
8
25
|
// and OneWordGuildName -> O
|
|
9
|
-
|
|
10
|
-
|
|
26
|
+
if (typeof props.guildName !== 'string') {
|
|
27
|
+
console.warn('[discord-html-transcripts-fix] the server has no name (an unavailable guild?); the header shows none');
|
|
28
|
+
}
|
|
29
|
+
const words = String(props.guildName ?? '').split(/\s+/).filter(Boolean);
|
|
30
|
+
const placeholder = words.length > 1 ? firstCharacter(words[0]) + firstCharacter(words[1]) : firstCharacter(words[0] ?? '');
|
|
11
31
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "discord-header", children: [(0, jsx_runtime_1.jsx)("div", { className: "discord-header-icon", children: props.guildIcon ? ((0, jsx_runtime_1.jsx)("img", { src: props.guildIcon, alt: "guild icon" })) : ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("span", { children: placeholder }) })) }), (0, jsx_runtime_1.jsxs)("div", { className: "discord-header-text", children: [(0, jsx_runtime_1.jsx)("div", { className: "discord-header-text-guild", children: props.guildName }), (0, jsx_runtime_1.jsxs)("div", { className: "discord-header-text-channel", children: ["#", props.channelName] }), props.children] })] }));
|
|
12
32
|
}
|
|
13
33
|
//# sourceMappingURL=TranscriptHeader.js.map
|
|
@@ -32,6 +32,44 @@ function hexToRgba(hex, alpha) {
|
|
|
32
32
|
return `rgba(${r},${g},${b},${alpha})`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// discord-markdown-parser ends a text node before every character outside
|
|
36
|
+
// [0-9A-Za-z] and whitespace, and it counts UTF-16 code units. A character outside
|
|
37
|
+
// the Basic Multilingual Plane — fancy-font letters, rare CJK, symbols — therefore
|
|
38
|
+
// arrives as two text nodes holding one lone surrogate half each, and encoding the
|
|
39
|
+
// page turned every half into U+FFFD. Adjacent text nodes are joined again, which
|
|
40
|
+
// also stops non-ASCII text from becoming one node (and one markup separator) per
|
|
41
|
+
// character. Plain text content is all that is merged; nothing else changes.
|
|
42
|
+
function mergeTextNodes(nodes) {
|
|
43
|
+
if (!Array.isArray(nodes)) return nodes;
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const node of nodes) {
|
|
46
|
+
if (Array.isArray(node)) {
|
|
47
|
+
out.push(mergeTextNodes(node));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!node || typeof node !== 'object') {
|
|
51
|
+
out.push(node);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const previous = out[out.length - 1];
|
|
55
|
+
if (node.type === 'text' && typeof node.content === 'string'
|
|
56
|
+
&& previous && !Array.isArray(previous) && previous.type === 'text' && typeof previous.content === 'string') {
|
|
57
|
+
out[out.length - 1] = Object.assign({}, previous, { content: previous.content + node.content });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const merged = Object.assign({}, node);
|
|
61
|
+
if (Array.isArray(node.content)) merged.content = mergeTextNodes(node.content);
|
|
62
|
+
if (Array.isArray(node.items)) merged.items = mergeTextNodes(node.items);
|
|
63
|
+
out.push(merged);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseMarkdown(source, mode) {
|
|
69
|
+
const parsed = discord_markdown_parser_1.default(source, mode);
|
|
70
|
+
return mergeTextNodes(Array.isArray(parsed) ? parsed : [parsed]);
|
|
71
|
+
}
|
|
72
|
+
|
|
35
73
|
// Discord-specific block-level markdown that discord-markdown-parser misses in inline mode.
|
|
36
74
|
// Run BEFORE the parser, extract heading/subtext lines, parse the rest, splice back together.
|
|
37
75
|
const HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
|
|
@@ -47,9 +85,7 @@ function preParseDiscord(content, mode) {
|
|
|
47
85
|
|
|
48
86
|
const flushInline = () => {
|
|
49
87
|
if (!inlineBuffer) return;
|
|
50
|
-
|
|
51
|
-
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
52
|
-
nodes.push(...arr);
|
|
88
|
+
nodes.push(...parseMarkdown(inlineBuffer, mode));
|
|
53
89
|
inlineBuffer = '';
|
|
54
90
|
};
|
|
55
91
|
|
|
@@ -77,16 +113,14 @@ function preParseDiscord(content, mode) {
|
|
|
77
113
|
if (h) {
|
|
78
114
|
flushInline();
|
|
79
115
|
const level = h[1].length;
|
|
80
|
-
|
|
81
|
-
nodes.push({ type: 'heading', level, content: Array.isArray(headingInner) ? headingInner : [headingInner] });
|
|
116
|
+
nodes.push({ type: 'heading', level, content: parseMarkdown(h[2], mode) });
|
|
82
117
|
continue;
|
|
83
118
|
}
|
|
84
119
|
|
|
85
120
|
const s = line.match(SUBTEXT_RE);
|
|
86
121
|
if (s) {
|
|
87
122
|
flushInline();
|
|
88
|
-
|
|
89
|
-
nodes.push({ type: 'subtext', content: Array.isArray(innerNodes) ? innerNodes : [innerNodes] });
|
|
123
|
+
nodes.push({ type: 'subtext', content: parseMarkdown(s[1], mode) });
|
|
90
124
|
continue;
|
|
91
125
|
}
|
|
92
126
|
|
|
@@ -103,14 +137,15 @@ function preParseDiscord(content, mode) {
|
|
|
103
137
|
|
|
104
138
|
async function MessageContent({ content, context }) {
|
|
105
139
|
if (context.type === RenderType.REPLY && content.length > 180) {
|
|
106
|
-
|
|
140
|
+
// Not through the middle of a surrogate pair: half of one cannot be encoded.
|
|
141
|
+
content = (0, utils_1.truncateText)(content, 180) + '...';
|
|
107
142
|
}
|
|
108
143
|
const mode = context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal';
|
|
109
144
|
// REPLY mode is always inline-only (heading inside a reply preview makes no sense).
|
|
110
145
|
// For all other modes (NORMAL/EMBED/WEBHOOK) we pre-process headings/subtext/code-fences
|
|
111
146
|
// so they render correctly even though discord-markdown-parser is inline-only.
|
|
112
147
|
const parsed = context.type === RenderType.REPLY
|
|
113
|
-
?
|
|
148
|
+
? parseMarkdown(content, mode)
|
|
114
149
|
: preParseDiscord(content, mode);
|
|
115
150
|
|
|
116
151
|
const nodes = Array.isArray(parsed) ? parsed : [parsed];
|
|
@@ -74,7 +74,7 @@ async function DiscordMessage({ message, context }) {
|
|
|
74
74
|
: '';
|
|
75
75
|
const authorName = (message.member?.nickname || message.author?.displayName || message.author?.username || '').toLowerCase();
|
|
76
76
|
const ts = message.createdAt instanceof Date ? message.createdAt.getTime() : 0;
|
|
77
|
-
const lowerText = (message.content || '').toLowerCase()
|
|
77
|
+
const lowerText = (0, utils_1.truncateText)((message.content || '').toLowerCase(), 4096);
|
|
78
78
|
const mediaFlags = detectMediaFlags(message);
|
|
79
79
|
const hasImage = mediaFlags.hasImage;
|
|
80
80
|
const hasEmbed = mediaFlags.hasEmbed;
|
|
@@ -198,7 +198,7 @@ async function DiscordMessage({ message, context }) {
|
|
|
198
198
|
isSilent && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-silent", title: "Silent message (no notification)", children: '🔕 Silent' }),
|
|
199
199
|
activity && (0, jsx_runtime_1.jsxs)("div", { className: "dht-activity", children: [
|
|
200
200
|
(0, jsx_runtime_1.jsx)("span", { className: "dht-activity-icon", children: '🎮' }),
|
|
201
|
-
(0, jsx_runtime_1.jsxs)("span", { children: [activity.type, ' invite', activity.partyId ? ` · Party ${activity.partyId.slice(-6)}` : ''] })
|
|
201
|
+
(0, jsx_runtime_1.jsxs)("span", { children: [activity.type, ' invite', activity.partyId ? ` · Party ${Array.from(activity.partyId).slice(-6).join('')}` : ''] })
|
|
202
202
|
] }),
|
|
203
203
|
editedAtIso && (0, jsx_runtime_1.jsx)("span", { className: "dht-edit-marker", title: editedAtTitle || editedAtIso, "data-edit-iso": editedAtIso, "data-i18n": "edited", "data-i18n-params": JSON.stringify({ time: editedAtLabel || editedAtIso }), children: '(' + t(context, 'edited', 'edited') + ')' }),
|
|
204
204
|
Array.isArray(message.editHistory) && message.editHistory.length > 0 && renderEditHistory(message.editHistory, context),
|
|
@@ -270,7 +270,7 @@ function renderThread(thread, context) {
|
|
|
270
270
|
return (0, jsx_runtime_1.jsxs)("discord-thread", { slot: "thread", name: thread.name, cta, children: [
|
|
271
271
|
badges,
|
|
272
272
|
thread.lastMessage
|
|
273
|
-
? (0, jsx_runtime_1.jsx)("discord-thread-message", { profile: thread.lastMessage.author.id, children: (0, jsx_runtime_1.jsx)(content_1.default, { content: thread.lastMessage.content.length > 128 ? thread.lastMessage.content
|
|
273
|
+
? (0, jsx_runtime_1.jsx)("discord-thread-message", { profile: thread.lastMessage.author.id, children: (0, jsx_runtime_1.jsx)(content_1.default, { content: thread.lastMessage.content.length > 128 ? (0, utils_1.truncateText)(thread.lastMessage.content, 125) + '...' : thread.lastMessage.content, context: Object.assign({}, context, { type: content_1.RenderType.REPLY }) }) })
|
|
274
274
|
: 'Thread messages not saved.'
|
|
275
275
|
] });
|
|
276
276
|
}
|
|
@@ -201,7 +201,7 @@ async function SystemMessage({ message, context }) {
|
|
|
201
201
|
return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, timestamp: ts, type: "boost", children: [
|
|
202
202
|
(0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
|
|
203
203
|
' ',
|
|
204
|
-
t(ctx, 'roleSubPurchased', 'subscribed to {role}!').replace('{role}', roleName),
|
|
204
|
+
t(ctx, 'roleSubPurchased', 'subscribed to {role}!').replace('{role}', () => roleName),
|
|
205
205
|
] }, message.id));
|
|
206
206
|
}
|
|
207
207
|
|
package/dist/utils/utils.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.safeHref = safeHref;
|
|
|
12
12
|
exports.safeImageSrc = safeImageSrc;
|
|
13
13
|
exports.safeLinkHref = safeLinkHref;
|
|
14
14
|
exports.describeError = describeError;
|
|
15
|
+
exports.truncateText = truncateText;
|
|
15
16
|
exports.safeColor = safeColor;
|
|
16
17
|
exports.safeImageMime = safeImageMime;
|
|
17
18
|
exports.escapeHtml = escapeHtml;
|
|
@@ -228,6 +229,16 @@ function safeLinkHref(url) {
|
|
|
228
229
|
return safeHref(url);
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
// Cuts `text` to at most `max` UTF-16 code units without cutting through a surrogate
|
|
233
|
+
// pair. Half of a pair cannot be encoded: it ends up as U+FFFD in the page.
|
|
234
|
+
function truncateText(text, max) {
|
|
235
|
+
if (typeof text !== 'string' || text.length <= max) return text;
|
|
236
|
+
let end = max;
|
|
237
|
+
const last = text.charCodeAt(end - 1);
|
|
238
|
+
if (last >= 0xd800 && last <= 0xdbff) end--; // a first half: leave it out with its second half
|
|
239
|
+
return text.slice(0, end);
|
|
240
|
+
}
|
|
241
|
+
|
|
231
242
|
// An error for a log line: its message and stack only. A whole error object can
|
|
232
243
|
// carry what a caller's HTTP client attached to it, request headers included.
|
|
233
244
|
function describeError(err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discord-html-transcripts-fix",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "A nicely formatted html transcript generator for discord.js. Bugfix fork with support for the latest discord.js and Components v2.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|