discord-html-transcripts-fix 1.1.0 → 1.2.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.
- package/README.md +86 -78
- package/dist/downloader/images.js +60 -85
- package/dist/generator/index.js +194 -43
- package/dist/generator/renderers/attachment.js +27 -36
- package/dist/generator/renderers/components/Container.js +36 -14
- package/dist/generator/renderers/components/Media Gallery.js +24 -18
- package/dist/generator/renderers/components.js +7 -2
- package/dist/generator/renderers/content.js +48 -48
- package/dist/generator/renderers/embed.js +55 -34
- package/dist/generator/renderers/message.js +159 -46
- package/dist/generator/renderers/reply.js +47 -39
- package/dist/generator/renderers/systemMessage.js +131 -70
- package/dist/generator/transcript.js +42 -56
- package/dist/index.js +59 -84
- package/dist/static/client.js +216 -208
- package/dist/types.js +1 -0
- package/dist/utils/buildProfiles.js +51 -59
- package/dist/utils/utils.js +73 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,77 @@
|
|
|
1
|
-
# `discord-html-transcripts`
|
|
1
|
+
# `discord-html-transcripts-fix`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A nicely formatted HTML transcript generator for discord.js with full **Components V2** support, an **interactive viewer** (search, lightbox, mention popups, i18n) and **hardened security**.
|
|
4
|
+
|
|
5
|
+
Forked from [discord-html-transcripts](https://github.com/ItzDerock/discord-html-transcripts).
|
|
4
6
|
|
|
5
7
|
## Changes compared to the original
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
9
|
+
### Security
|
|
10
|
+
- **Critical fix**: `</script>` breakout via inlined JSON is prevented (`<`, `>`, `&`, U+2028/2029 are escaped)
|
|
11
|
+
- **Critical fix**: markdown links with `javascript:`, `data:`, `vbscript:` and similar dangerous URI schemes are rewritten to `#`
|
|
12
|
+
- **Hardened**: inline `style="color:…"` sinks in the mention popup are hex-validated to block CSS injection
|
|
13
|
+
- **Hardened**: `data:` URI MIME types from the image downloader are restricted to image types only (no `text/html` smuggling)
|
|
14
|
+
- **Fixed**: `process.exit(1)` on discord.js version mismatch removed — library no longer kills the host bot
|
|
15
|
+
|
|
16
|
+
### Robustness
|
|
17
|
+
- **Fix**: invalid Discord timestamp markers (`<t:abc:F>`, oversized values) no longer abort the transcript with `RangeError`
|
|
18
|
+
- **Fix**: per-AST-node error boundary in `MessageSingleASTNode` and per-message error boundary in `DiscordMessage` — one broken message can never kill the whole render any more
|
|
19
|
+
- **Fix**: `parseDiscordEmoji` no longer throws on deleted reactions with `emoji.name === null`
|
|
20
|
+
- **Fix**: `formatBytes(null/undefined/NaN)` no longer returns `NaN undefined`
|
|
21
|
+
- **Fix**: `createTranscript` slice uses the resolved limit instead of the raw `limit`
|
|
22
|
+
- **Fix**: embed fields are rendered through a proper async component (was an inline `async` arrow inside `.map()` — undefined React behaviour)
|
|
23
|
+
- **Fix**: `JoinMessage` text is now deterministic per message ID — re-rendering the same channel always yields the same join line
|
|
24
|
+
- **Fix**: random `console.log` calls in production paths replaced by the `debug` namespace
|
|
25
|
+
|
|
26
|
+
### New: interactive viewer
|
|
27
|
+
- **Clickable mentions** — clicking a user / role / channel mention opens a Discord-style popup with avatar, display name, username, all roles (colored pills), server-since, account-since, color, member count, channel topic, etc.
|
|
28
|
+
- **Search bar** — Ctrl+F is intercepted, live highlighting with "x of n matches", prev / next, Enter / Shift+Enter to navigate, Escape to close
|
|
29
|
+
- **Image lightbox** — click any image to open fullscreen, arrow keys to nav, Escape to close
|
|
30
|
+
- **Participant sidebar (TOC)** — toggle button (top-left), sorted list of every participant, click to jump to their first message
|
|
31
|
+
- **Back-to-top** floating button fades in after scrolling
|
|
32
|
+
- **Date separators** between messages — `— Tuesday, May 13 2026 —` style, automatically inserted on day boundaries
|
|
33
|
+
- **Stats footer** — `324 messages · 12 participants · 5 images` (auto-computed)
|
|
34
|
+
- **Language switcher** (i18n) — built-in English and German dictionaries, switch live in the viewer. Pass your own via `options.i18n: { de: { … } }` to extend
|
|
35
|
+
|
|
36
|
+
### New: more Discord content rendered
|
|
37
|
+
- **Stickers** — image, name, alt
|
|
38
|
+
- **Polls** — question, answer bars with vote counts and percentages, expiry
|
|
39
|
+
- **Forwarded messages (`messageSnapshots`)** — quoted-block style with original author
|
|
40
|
+
- **Voice messages** — `🎤` indicator, inline SVG waveform from `attachment.waveform`, duration
|
|
41
|
+
- **Edit timestamp tooltip** on `(edited)` marker
|
|
42
|
+
- **Optional edit history** — pass `message.editHistory = [{content, editedAt}, …]` and the viewer shows a collapsible `<details>` block
|
|
43
|
+
- **Pinned indicator** (`📌` badge on pinned messages)
|
|
44
|
+
- **App badge** for application/bot messages with `applicationId`
|
|
45
|
+
- **Suppressed embeds flag** is honored — when set, embeds aren't rendered (and a small `(embeds hidden)` note is shown instead)
|
|
46
|
+
- **Cross-guild replies** are no longer silently dropped — they show a "Message from another server" pill
|
|
47
|
+
- **Burst / super-reactions** are flagged
|
|
48
|
+
- **Thread state badges** — `Archived`, `Locked`
|
|
49
|
+
- **GIFV / animated GIFs** are rendered with `<video autoplay loop muted>` like in Discord
|
|
50
|
+
- **Attachment description (alt text)** is used as `alt`/`title`
|
|
51
|
+
- **`<id:guide>`, `<id:browse>`, `<id:customize>`** etc. pseudo-channels show as styled pills with proper labels
|
|
52
|
+
- **`</cmd:id>` slash command mentions** render as blue pills
|
|
53
|
+
- **Embed video** link and **embed provider** ("YouTube" etc.) are shown
|
|
54
|
+
- **`RoleSubscriptionPurchase`** system message renders properly
|
|
55
|
+
- **`AutoModerationAction`** shows the matched rule name and blocked content excerpt
|
|
56
|
+
- **Container component** with `accent_color` (left border) and `spoiler` support
|
|
57
|
+
- **Media gallery** per-item spoiler support
|
|
58
|
+
- Many additional system message types (`ChannelNameChange`, `ChannelIconChange`, `ThreadCreated`, `ChatInputCommand`, `ContextMenuCommand`, `Call`, `ChannelFollowAdd`, `RecipientRemove`, guild incident reports, poll result)
|
|
59
|
+
|
|
60
|
+
### Performance
|
|
61
|
+
- **Single-pass profile collector** — `buildAllContext` builds profiles + extended dicts in one walk over the message list
|
|
62
|
+
- **Image downloader is concurrent** — bounded pool (default 6, configurable via `withConcurrency`) instead of sequential
|
|
63
|
+
- **`@skyra/discord-components-core` version is pinned** to an exact resolved version → CDN cacheable
|
|
64
|
+
- **Inline JSON** is shipped via `<script type="application/json">` so it doesn't block HTML parse
|
|
65
|
+
- **Emoji URL resolution** is memoized
|
|
66
|
+
- **`returnType: 'stream'`** option streams the rendered HTML out instead of buffering — usable for 5,000+ message tickets
|
|
67
|
+
|
|
68
|
+
### DX
|
|
69
|
+
- **`react`, `react-dom`, `debug` moved into regular dependencies** so users don't install them manually (`debug` was actually a missing runtime dep in the original — `images.js` requires it)
|
|
70
|
+
- TypeScript build target unchanged; only `discord.js` remains a peer dependency
|
|
22
71
|
|
|
23
72
|
## Requirements
|
|
24
73
|
|
|
25
|
-
Only
|
|
74
|
+
Only `discord.js` as a peer dependency. Everything else is auto-installed:
|
|
26
75
|
|
|
27
76
|
```bash
|
|
28
77
|
npm install discord-html-transcripts-fix
|
|
@@ -30,76 +79,35 @@ npm install discord-html-transcripts-fix
|
|
|
30
79
|
|
|
31
80
|
## Usage
|
|
32
81
|
|
|
33
|
-
### Example usage using the built in message fetcher.
|
|
34
|
-
|
|
35
82
|
```js
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
83
|
+
const { createTranscript } = require('discord-html-transcripts-fix');
|
|
84
|
+
|
|
85
|
+
const attachment = await createTranscript(channel, {
|
|
86
|
+
// existing options
|
|
87
|
+
limit: -1,
|
|
88
|
+
saveImages: false,
|
|
89
|
+
poweredBy: true,
|
|
90
|
+
// new options
|
|
91
|
+
language: 'de', // 'en' (default) or 'de' — also exposed live in the viewer
|
|
92
|
+
i18n: { de: { joined: 'kam an' } }, // optional overrides per language
|
|
93
|
+
returnType: 'stream', // 'buffer' | 'string' | 'attachment' (default) | 'stream'
|
|
46
94
|
});
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Or if you prefer, you can pass in your own messages.
|
|
50
|
-
|
|
51
|
-
```js
|
|
52
|
-
const discordTranscripts = require('discord-html-transcripts');
|
|
53
|
-
// or (if using typescript) import * as discordTranscripts from 'discord-html-transcripts';
|
|
54
95
|
|
|
55
|
-
|
|
56
|
-
const channel = someWayToGetChannel(); // Used for ticket name, guild icon, and guild name
|
|
57
|
-
|
|
58
|
-
// Must be awaited
|
|
59
|
-
const attachment = await discordTranscripts.generateFromMessages(messages, channel);
|
|
60
|
-
|
|
61
|
-
channel.send({
|
|
62
|
-
files: [attachment],
|
|
63
|
-
});
|
|
96
|
+
channel.send({ files: [attachment] });
|
|
64
97
|
```
|
|
65
98
|
|
|
66
|
-
|
|
99
|
+
### Hooking edit history (optional)
|
|
67
100
|
|
|
68
|
-
|
|
69
|
-
**All configuration options are optional!**
|
|
70
|
-
|
|
71
|
-
### Built in Message Fetcher
|
|
101
|
+
If your bot tracks message edits, attach the history to the message before passing it to the renderer:
|
|
72
102
|
|
|
73
103
|
```js
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
saveImages: false, // Download all images and include the image data in the HTML (allows viewing the image even after it has been deleted) (! WILL INCREASE FILE SIZE !)
|
|
79
|
-
footerText: "Exported {number} message{s}", // Change text at footer, don't forget to put {number} to show how much messages got exported, and {s} for plural
|
|
80
|
-
callbacks: {
|
|
81
|
-
// register custom callbacks for the following:
|
|
82
|
-
resolveChannel: (channelId: string) => Awaitable<Channel | null>,
|
|
83
|
-
resolveUser: (userId: string) => Awaitable<User | null>,
|
|
84
|
-
resolveRole: (roleId: string) => Awaitable<Role | null>,
|
|
85
|
-
resolveImageSrc: (
|
|
86
|
-
attachment: APIAttachment,
|
|
87
|
-
message: APIMessage
|
|
88
|
-
) => Awaitable<string | null | undefined>
|
|
89
|
-
},
|
|
90
|
-
poweredBy: true, // Whether to include the "Powered by discord-html-transcripts" footer
|
|
91
|
-
hydrate: true, // Whether to hydrate the html server-side
|
|
92
|
-
filter: (message) => true // Filter messages, e.g. (message) => !message.author.bot
|
|
93
|
-
});
|
|
104
|
+
message.editHistory = [
|
|
105
|
+
{ content: 'first version', editedAt: new Date('2025-05-13T11:02Z') },
|
|
106
|
+
{ content: 'corrected version', editedAt: new Date('2025-05-13T11:05Z') },
|
|
107
|
+
];
|
|
94
108
|
```
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```js
|
|
99
|
-
const attachment = await discordTranscripts.generateFromMessages(messages, channel, {
|
|
100
|
-
// Same as createTranscript, except no limit or filter
|
|
101
|
-
});
|
|
102
|
-
```
|
|
110
|
+
The viewer will then render a collapsible `<details>` block next to the `(edited)` marker.
|
|
103
111
|
|
|
104
112
|
## Credits
|
|
105
113
|
|
|
@@ -1,36 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
2
|
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
3
|
+
var ownKeys = function(o){ownKeys = Object.getOwnPropertyNames || function (o){var ar=[];for (var k in o) if (Object.prototype.hasOwnProperty.call(o,k)) ar[ar.length]=k;return ar;};return ownKeys(o);};
|
|
4
|
+
return function (mod){if (mod && mod.__esModule) return mod;var result={};if (mod != null) for (var k=ownKeys(mod), i=0; i<k.length; i++) if (k[i] !== "default") Object.defineProperty(result,k[i],{get:function(){return mod[k[i]];}});Object.defineProperty(result,"default",{value:mod,enumerable:true});return result;};
|
|
34
5
|
})();
|
|
35
6
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -39,76 +10,80 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
10
|
exports.TranscriptImageDownloader = void 0;
|
|
40
11
|
const undici_1 = require("undici");
|
|
41
12
|
const debug_1 = __importDefault(require("debug"));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
13
|
+
const utils_1 = require("../utils/utils");
|
|
14
|
+
|
|
15
|
+
// Small inline concurrency limiter — no extra dep
|
|
16
|
+
function createLimiter(max) {
|
|
17
|
+
let active = 0;
|
|
18
|
+
const queue = [];
|
|
19
|
+
const next = () => {
|
|
20
|
+
if (active >= max || queue.length === 0) return;
|
|
21
|
+
active++;
|
|
22
|
+
const { fn, resolve, reject } = queue.shift();
|
|
23
|
+
fn().then((v) => { active--; resolve(v); next(); }, (e) => { active--; reject(e); next(); });
|
|
24
|
+
};
|
|
25
|
+
return (fn) => new Promise((resolve, reject) => { queue.push({ fn, resolve, reject }); next(); });
|
|
26
|
+
}
|
|
27
|
+
|
|
45
28
|
class TranscriptImageDownloader {
|
|
46
29
|
constructor() {
|
|
47
30
|
this.log = TranscriptImageDownloader.log;
|
|
31
|
+
this.maxConcurrent = 6;
|
|
32
|
+
this._sharpModule = null;
|
|
33
|
+
this._limit = null;
|
|
48
34
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Sets the maximum file size for *each* individual image.
|
|
51
|
-
* @param size The maximum file size in kilobytes
|
|
52
|
-
*/
|
|
53
35
|
withMaxSize(size) {
|
|
54
36
|
this.maxFileSize = size;
|
|
55
37
|
return this;
|
|
56
38
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* @param convertToWebP Whether to convert the image to WebP format
|
|
62
|
-
*/
|
|
39
|
+
withConcurrency(n) {
|
|
40
|
+
this.maxConcurrent = Math.max(1, Number(n) || 1);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
63
43
|
withCompression(quality = 80, convertToWebP = false, options = {}) {
|
|
64
|
-
if (quality < 1 || quality > 100)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
44
|
+
if (quality < 1 || quality > 100) throw new Error('Quality must be between 1 and 100');
|
|
45
|
+
// Hoist the sharp import so we don't pay it per image
|
|
46
|
+
Promise.resolve()
|
|
47
|
+
.then(() => __importStar(require('sharp')))
|
|
48
|
+
.then((s) => { this._sharpModule = s; })
|
|
49
|
+
.catch((err) => {
|
|
50
|
+
this.log('sharp import failed: %s', err && err.message ? err.message : err);
|
|
51
|
+
});
|
|
71
52
|
this.compression = { quality, convertToWebP, options };
|
|
72
53
|
return this;
|
|
73
54
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Builds the image saving callback.
|
|
76
|
-
*/
|
|
77
55
|
build() {
|
|
56
|
+
if (!this._limit) this._limit = createLimiter(this.maxConcurrent);
|
|
78
57
|
return async (attachment) => {
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
// if the max file size is set, check if the file size is within the limit
|
|
83
|
-
if (this.maxFileSize && attachment.size > this.maxFileSize * 1024)
|
|
84
|
-
return undefined;
|
|
85
|
-
// fetch the image
|
|
86
|
-
this.log(`Fetching attachment ${attachment.id}: ${attachment.url}`);
|
|
87
|
-
const response = await (0, undici_1.request)(attachment.url).catch((err) => {
|
|
88
|
-
console.error(`[discord-html-transcripts] Failed to download image for transcript: `, err);
|
|
89
|
-
return null;
|
|
90
|
-
});
|
|
91
|
-
if (!response)
|
|
92
|
-
return undefined;
|
|
93
|
-
const mimetype = response.headers['content-type'];
|
|
94
|
-
const buffer = await response.body.arrayBuffer().then((res) => Buffer.from(res));
|
|
95
|
-
this.log(`Finished fetching ${attachment.id} (${buffer.length} bytes)`);
|
|
96
|
-
// if the compression options are set, compress the image
|
|
97
|
-
if (this.compression) {
|
|
98
|
-
const sharp = await Promise.resolve().then(() => __importStar(require('sharp')));
|
|
99
|
-
this.log(`Compressing ${attachment.id} with 'sharp'`);
|
|
100
|
-
const sharpbuf = await sharp
|
|
101
|
-
.default(buffer)
|
|
102
|
-
.webp(Object.assign({ quality: this.compression.quality, force: this.compression.convertToWebP, effort: 2 }, this.compression.options))
|
|
103
|
-
.toBuffer({ resolveWithObject: true });
|
|
104
|
-
this.log(`Finished compressing ${attachment.id} (${sharpbuf.info.size} bytes)`);
|
|
105
|
-
return `data:image/${sharpbuf.info.format};base64,${sharpbuf.data.toString('base64')}`;
|
|
106
|
-
}
|
|
107
|
-
// return the base64 string
|
|
108
|
-
return `data:${mimetype};base64,${buffer.toString('base64')}`;
|
|
58
|
+
if (!attachment.width || !attachment.height) return undefined;
|
|
59
|
+
if (this.maxFileSize && attachment.size > this.maxFileSize * 1024) return undefined;
|
|
60
|
+
return this._limit(() => this._fetchOne(attachment));
|
|
109
61
|
};
|
|
110
62
|
}
|
|
63
|
+
async _fetchOne(attachment) {
|
|
64
|
+
this.log('fetch %s %s', attachment.id, attachment.url);
|
|
65
|
+
const response = await (0, undici_1.request)(attachment.url).catch((err) => {
|
|
66
|
+
this.log('download failed for %s: %s', attachment.id, err && err.message ? err.message : err);
|
|
67
|
+
return null;
|
|
68
|
+
});
|
|
69
|
+
if (!response) return undefined;
|
|
70
|
+
const rawMime = response.headers['content-type'];
|
|
71
|
+
const mime = (0, utils_1.safeImageMime)(typeof rawMime === 'string' ? rawMime : '', 'image/png');
|
|
72
|
+
const arr = await response.body.arrayBuffer();
|
|
73
|
+
const buffer = Buffer.from(arr);
|
|
74
|
+
this.log('fetched %s (%d bytes, mime=%s)', attachment.id, buffer.length, mime);
|
|
75
|
+
if (this.compression) {
|
|
76
|
+
const sharp = this._sharpModule || (this._sharpModule = await __importStar(require('sharp')));
|
|
77
|
+
this.log('compressing %s with sharp', attachment.id);
|
|
78
|
+
const sharpbuf = await sharp
|
|
79
|
+
.default(buffer)
|
|
80
|
+
.webp(Object.assign({ quality: this.compression.quality, force: this.compression.convertToWebP, effort: 2 }, this.compression.options))
|
|
81
|
+
.toBuffer({ resolveWithObject: true });
|
|
82
|
+
return `data:image/${sharpbuf.info.format};base64,${sharpbuf.data.toString('base64')}`;
|
|
83
|
+
}
|
|
84
|
+
return `data:${mime};base64,${buffer.toString('base64')}`;
|
|
85
|
+
}
|
|
111
86
|
}
|
|
112
87
|
exports.TranscriptImageDownloader = TranscriptImageDownloader;
|
|
113
88
|
TranscriptImageDownloader.log = (0, debug_1.default)('discord-html-transcripts:TranscriptImageDownloader');
|
|
114
|
-
//# sourceMappingURL=images.js.map
|
|
89
|
+
//# sourceMappingURL=images.js.map
|