mediawiki-file-url 0.1.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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benjamin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # mediawiki-file-url
2
+ Convert MediaWiki filenames into its hashed upload URLs on wikis with `$wgHashedUploadDirectory` enabled.
3
+
4
+ ## Install
5
+ ```
6
+ npm i mediawiki-file-url
7
+ ```
8
+
9
+ ## Usage
10
+ ```js
11
+ import {
12
+ mwFileUrl,
13
+ mwWikiFileUrl,
14
+ mwSetBaseUrl,
15
+ mwWithBaseUrl,
16
+ } from "mediawiki-file-url";
17
+
18
+ // From a plain filename (default base URL is set to the TDS Wiki images root)
19
+ const url1 = mwFileUrl("WarlockLevel1.png");
20
+
21
+ // Set a module-wide default base URL once (used when you omit baseUrl)
22
+ mwSetBaseUrl("https://wiki.archlinux.org/images");
23
+ const url2 = mwFileUrl("Tango-edit-clear.svg");
24
+ const url3 = mwWikiFileUrl("File:Tango-edit-clear.svg");
25
+
26
+ // Or create bound converters for one base URL (recommended if you need multiple wikis)
27
+ const lol = mwWithBaseUrl("https://wiki.leagueoflegends.com/en-us/images");
28
+ const url4 = lol.mwFileUrl("Champions_Mesh_concept_03.jpg");
29
+ const url5 = lol.mwWikiFileUrl(" Image: Champions_Mesh_concept_02.jpg ");
30
+ ```
31
+
32
+ ## API
33
+ ### `mwFileUrl(filename, [baseUrl])`
34
+
35
+ - `filename` (string) - e.g. `"My Image.png"`
36
+ - `baseUrl` (string, optional) — if omitted, uses the configured default base URL (which is the Tower Defense Simulator Wiki)
37
+
38
+ Returns the hashed upload URL.
39
+
40
+ ### `mwWikiFileUrl(wikiSyntax, [baseUrl])`
41
+ - `wikiSyntax` (string) - must start with `File:` or `Image:` (whitespace is trimmed)
42
+ - `baseUrl` (string, optional) — if omitted, uses the configured default base URL
43
+
44
+ Extracts the filename and calls `mwFileUrl`.
45
+
46
+ ### `mwSetBaseUrl(baseUrl)`
47
+ Sets the module-wide default base URL used by `mwFileUrl` / `mwWikiFileUrl` when `baseUrl` is omitted.
48
+
49
+ ### `mwWithBaseUrl(baseUrl)`
50
+ Returns `{ mwFileUrl, mwWikiFileUrl }` functions bound to a specific `baseUrl`, so you don’t have to pass it each call.
51
+
52
+ ## Notes
53
+ - ESM only.
54
+ - If a wiki uses a custom hashed layout, it won't work.
package/index.js ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Itsy bitsy MD5 implementation since browsers don't support MD5.
3
+ * Based on the public domain implementation by Joseph Myers.
4
+ */
5
+ function md5(message) {
6
+ function safe_add(x, y) {
7
+ var lsw = (x & 0xffff) + (y & 0xffff);
8
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
9
+ return (msw << 16) | (lsw & 0xffff);
10
+ }
11
+
12
+ function bit_rol(num, cnt) {
13
+ return (num << cnt) | (num >>> (32 - cnt));
14
+ }
15
+
16
+ function md5_cmn(q, a, b, x, s, t) {
17
+ return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
18
+ }
19
+ function md5_ff(a, b, c, d, x, s, t) {
20
+ return md5_cmn((b & c) | (~b & d), a, b, x, s, t);
21
+ }
22
+ function md5_gg(a, b, c, d, x, s, t) {
23
+ return md5_cmn((b & d) | (c & ~d), a, b, x, s, t);
24
+ }
25
+ function md5_hh(a, b, c, d, x, s, t) {
26
+ return md5_cmn(b ^ c ^ d, a, b, x, s, t);
27
+ }
28
+ function md5_ii(a, b, c, d, x, s, t) {
29
+ return md5_cmn(c ^ (b | ~d), a, b, x, s, t);
30
+ }
31
+
32
+ function str2binl(str) {
33
+ var nblk = ((str.length + 8) >> 6) + 1;
34
+ var blks = Array(nblk * 16);
35
+ for (var i = 0; i < nblk * 16; i++) blks[i] = 0;
36
+ for (var i = 0; i < str.length; i++)
37
+ blks[i >> 2] |= str.charCodeAt(i) << ((i % 4) * 8);
38
+ blks[i >> 2] |= 0x80 << ((i % 4) * 8);
39
+ blks[nblk * 16 - 2] = str.length * 8;
40
+ return blks;
41
+ }
42
+
43
+ function binl2hex(binarray) {
44
+ var hex_tab = "0123456789abcdef";
45
+ var str = "";
46
+ for (var i = 0; i < binarray.length * 4; i++) {
47
+ str +=
48
+ hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xf) +
49
+ hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xf);
50
+ }
51
+ return str;
52
+ }
53
+
54
+ var x = str2binl(message);
55
+ var a = 1732584193;
56
+ var b = -271733879;
57
+ var c = -1732584194;
58
+ var d = 271733878;
59
+
60
+ for (var i = 0; i < x.length; i += 16) {
61
+ var olda = a;
62
+ var oldb = b;
63
+ var oldc = c;
64
+ var oldd = d;
65
+
66
+ a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
67
+ d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
68
+ c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
69
+ b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
70
+ a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
71
+ d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
72
+ c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
73
+ b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
74
+ a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
75
+ d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
76
+ c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
77
+ b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
78
+ a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
79
+ d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
80
+ c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
81
+ b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
82
+
83
+ a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
84
+ d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
85
+ c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
86
+ b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
87
+ a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
88
+ d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
89
+ c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
90
+ b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
91
+ a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
92
+ d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
93
+ c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
94
+ b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
95
+ a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
96
+ d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
97
+ c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
98
+ b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
99
+
100
+ a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
101
+ d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
102
+ c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
103
+ b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
104
+ a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
105
+ d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
106
+ c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
107
+ b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
108
+ a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
109
+ d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
110
+ c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
111
+ b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
112
+ a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
113
+ d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
114
+ c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
115
+ b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
116
+
117
+ a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
118
+ d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
119
+ c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
120
+ b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
121
+ a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
122
+ d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
123
+ c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
124
+ b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
125
+ a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
126
+ d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
127
+ c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
128
+ b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
129
+ a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
130
+ d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
131
+ c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
132
+ b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
133
+
134
+ a = safe_add(a, olda);
135
+ b = safe_add(b, oldb);
136
+ c = safe_add(c, oldc);
137
+ d = safe_add(d, oldd);
138
+ }
139
+ return binl2hex([a, b, c, d]);
140
+ }
141
+
142
+ /**
143
+ * Default base URL used by mwFileUrl/mwWikiFileUrl when no baseUrl argument is provided.
144
+ * This can be changed at runtime via mwSetBaseUrl().
145
+ */
146
+ const DEFAULT_BASE_URL =
147
+ "https://static.wikia.nocookie.net/tower-defense-sim/images";
148
+ let defaultBaseUrl = DEFAULT_BASE_URL;
149
+
150
+ /**
151
+ * Set the module-wide default base URL used by mwFileUrl/mwWikiFileUrl
152
+ * when no baseUrl argument is provided.
153
+ *
154
+ * @param {string} baseUrl
155
+ */
156
+ export function mwSetBaseUrl(baseUrl) {
157
+ if (!baseUrl || typeof baseUrl !== "string") {
158
+ throw new Error("Base URL must be a non-empty string");
159
+ }
160
+ defaultBaseUrl = baseUrl;
161
+ }
162
+
163
+ /**
164
+ * Get the current module-wide default base URL.
165
+ * @returns {string}
166
+ */
167
+ export function getDefaultBaseUrl() {
168
+ return defaultBaseUrl;
169
+ }
170
+
171
+ /**
172
+ * Converts a wiki filename to a MediaWiki file URL.
173
+ * Should work for any MediaWiki instance that uses
174
+ * any unmodified $wgHashedUploadDirectory implementation.
175
+ *
176
+ * @param {string} filename - The filename (for example, "WarlockLevel1.png")
177
+ * @param {string} [baseUrl] - The base URL for images (defaults to configured defaultBaseUrl)
178
+ * @returns {string} The full MediaWiki URL
179
+ * @throws {Error} If filename is invalid or hashing fails
180
+ */
181
+ export function mwFileUrl(filename, baseUrl) {
182
+ if (!filename || typeof filename !== "string") {
183
+ throw new Error("Filename must be a non-empty string");
184
+ }
185
+
186
+ const effectiveBaseUrl = baseUrl ?? defaultBaseUrl;
187
+
188
+ try {
189
+ // Normalize spaces to underscores (cuz MediaWiki does that)
190
+ const normalizedFilename = filename.replace(/ /g, "_");
191
+
192
+ // Yada yada give me the md5
193
+ const md5Hash = md5(normalizedFilename);
194
+ const firstChar = md5Hash.charAt(0);
195
+ const firstTwoChars = md5Hash.substring(0, 2);
196
+ const cleanBaseUrl = effectiveBaseUrl.endsWith("/")
197
+ ? effectiveBaseUrl.slice(0, -1)
198
+ : effectiveBaseUrl;
199
+
200
+ // baseUrl/a/ab/Filename.png
201
+ const finalUrl = `${cleanBaseUrl}/${firstChar}/${firstTwoChars}/${encodeURIComponent(normalizedFilename)}`;
202
+
203
+ return finalUrl;
204
+ } catch (error) {
205
+ throw new Error(
206
+ `Error converting filename to MediaWiki URL: ${error.message}`,
207
+ );
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Create converters bound to a baseUrl so one does not have to pass it each time.
213
+ *
214
+ * @param {string} baseUrl
215
+ * @returns {{ mwFileUrl: (filename: string) => string, mwWikiFileUrl: (wikiSyntax: string) => string }}
216
+ */
217
+ export function mwWithBaseUrl(baseUrl) {
218
+ if (!baseUrl || typeof baseUrl !== "string") {
219
+ throw new Error("Base URL must be a non-empty string");
220
+ }
221
+
222
+ return {
223
+ mwFileUrl: (filename) => mwFileUrl(filename, baseUrl),
224
+ mwWikiFileUrl: (wikiSyntax) => mwWikiFileUrl(wikiSyntax, baseUrl),
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Converts a File:/Image: syntax to a MediaWiki URL
230
+ * @param {string} wikiSyntax - The wiki syntax (for example, "File:WarlockLevel1.png")
231
+ * @param {string} [baseUrl] - The base URL for images
232
+ * @returns {string} The full MediaWiki URL
233
+ * @throws {Error} If syntax is invalid or conversion fails
234
+ */
235
+ export function mwWikiFileUrl(wikiSyntax, baseUrl) {
236
+ if (!wikiSyntax || typeof wikiSyntax !== "string") {
237
+ throw new Error("Wiki syntax must be a non-empty string");
238
+ }
239
+
240
+ const trimmed = wikiSyntax.trim();
241
+
242
+ if (trimmed.startsWith("File:")) {
243
+ const filename = trimmed.substring(5).trim();
244
+ return mwFileUrl(filename, baseUrl);
245
+ }
246
+
247
+ if (trimmed.startsWith("Image:")) {
248
+ const filename = trimmed.substring(6).trim();
249
+ return mwFileUrl(filename, baseUrl);
250
+ }
251
+
252
+ throw new Error('Wiki syntax must start with "File:" or "Image:"');
253
+ }
254
+
255
+ export default {
256
+ mwFileUrl,
257
+ mwWikiFileUrl,
258
+ mwSetBaseUrl,
259
+ getDefaultBaseUrl,
260
+ mwWithBaseUrl,
261
+ };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "mediawiki-file-url",
3
+ "version": "0.1.0",
4
+ "description": "Convert MediaWiki filenames into hashed upload URLs",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node test.js"
12
+ },
13
+ "keywords": [
14
+ "mediawiki",
15
+ "md5"
16
+ ],
17
+ "author": "t7ru",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/t7ru/mediawiki-file-url.git"
22
+ }
23
+ }