@lizardbyte/shared-web 2024.901.15440
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/LICENSE +661 -0
- package/README.md +39 -0
- package/dist/crowdin-web-widget.js +2 -0
- package/dist/crowdin-web-widget.js.map +1 -0
- package/dist/crowdin.js +2 -0
- package/dist/crowdin.js.map +1 -0
- package/dist/discord.js +3 -0
- package/dist/discord.js.LICENSE.txt +1 -0
- package/dist/discord.js.map +1 -0
- package/dist/levenshtein-distance.js +2 -0
- package/dist/levenshtein-distance.js.map +1 -0
- package/dist/lizardbyte-css.css +3 -0
- package/dist/lizardbyte-css.css.map +1 -0
- package/dist/lizardbyte-css.js +0 -0
- package/dist/load-script.js +2 -0
- package/dist/load-script.js.map +1 -0
- package/dist/random-quote.js +3 -0
- package/dist/random-quote.js.LICENSE.txt +1 -0
- package/dist/random-quote.js.map +1 -0
- package/dist/ranking-sorter.js +2 -0
- package/dist/ranking-sorter.js.map +1 -0
- package/dist/sleep.js +2 -0
- package/dist/sleep.js.map +1 -0
- package/package.json +58 -0
- package/src/css/lizardbyte.scss +171 -0
- package/src/js/crowdin-web-widget.js +22 -0
- package/src/js/crowdin.js +53 -0
- package/src/js/discord.js +29 -0
- package/src/js/levenshtein-distance.js +36 -0
- package/src/js/lizardbyte-css.js +1 -0
- package/src/js/load-script.js +22 -0
- package/src/js/random-quote.js +25 -0
- package/src/js/ranking-sorter.js +26 -0
- package/src/js/sleep.js +10 -0
- package/tests/levenshtein-distance.test.js +26 -0
- package/tests/load-script.test.js +48 -0
- package/tests/random-quote.test.js +77 -0
- package/tests/ranking-sorter.test.js +55 -0
- package/tests/sleep.test.js +41 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function levenshteinDistance(a, b) {
|
|
2
|
+
if (a.length === 0) return b.length;
|
|
3
|
+
if (b.length === 0) return a.length;
|
|
4
|
+
|
|
5
|
+
let matrix = [];
|
|
6
|
+
|
|
7
|
+
// increment along the first column of each row
|
|
8
|
+
let i;
|
|
9
|
+
for (i = 0; i <= b.length; i++) {
|
|
10
|
+
matrix[i] = [i];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// increment each column in the first row
|
|
14
|
+
let j;
|
|
15
|
+
for (j = 0; j <= a.length; j++) {
|
|
16
|
+
matrix[0][j] = j;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fill in the rest of the matrix
|
|
20
|
+
for (i = 1; i <= b.length; i++) {
|
|
21
|
+
for (j = 1; j <= a.length; j++) {
|
|
22
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
23
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
24
|
+
} else {
|
|
25
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
|
|
26
|
+
Math.min(matrix[i][j - 1] + 1, // insertion
|
|
27
|
+
matrix[i - 1][j] + 1)); // deletion
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// return the percentage of the levenshtein distance
|
|
33
|
+
return (1 - (matrix[b.length][a.length] / Math.max(a.length, b.length))) * 100
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = levenshteinDistance;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../css/lizardbyte.scss";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a script asynchronously, and add it to the DOM. Optionally, call a callback when the script has loaded.
|
|
3
|
+
* @param url The URL of the script to load.
|
|
4
|
+
* @param callback An optional callback to call when the script has loaded.
|
|
5
|
+
*/
|
|
6
|
+
function loadScript(url, callback) {
|
|
7
|
+
const script = document.createElement('script');
|
|
8
|
+
script.src = url;
|
|
9
|
+
script.async = true;
|
|
10
|
+
|
|
11
|
+
script.onload = () => {
|
|
12
|
+
if (callback) callback(null, script);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
script.onerror = () => {
|
|
16
|
+
if (callback) callback(new Error(`Failed to load script: ${url}`));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
document.head.appendChild(script);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = loadScript;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
let quoteCache = null;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch a random quote from our API.
|
|
5
|
+
* @returns {Promise<*>} A promise that resolves with a random quote
|
|
6
|
+
*/
|
|
7
|
+
async function fetchRandomQuote() {
|
|
8
|
+
if (!quoteCache) {
|
|
9
|
+
const response = await fetch('https://app.lizardbyte.dev/uno/random-quotes/games.json');
|
|
10
|
+
quoteCache = await response.json();
|
|
11
|
+
}
|
|
12
|
+
return quoteCache[Math.floor(Math.random() * quoteCache.length)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reset the quote cache.
|
|
17
|
+
*/
|
|
18
|
+
function resetQuoteCache() {
|
|
19
|
+
quoteCache = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
fetchRandomQuote,
|
|
24
|
+
resetQuoteCache,
|
|
25
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sorts an array of objects by two keys in descending order.
|
|
3
|
+
* @param firstKey The primary key to sort by.
|
|
4
|
+
* @param secondKey The secondary key to sort by, in case the first key is equal.
|
|
5
|
+
* @returns {(function(*, *): (number))|*} The sorting function.
|
|
6
|
+
*/
|
|
7
|
+
let rankingSorter = function (firstKey, secondKey) {
|
|
8
|
+
return function(a, b) {
|
|
9
|
+
if (a[firstKey] > b[firstKey]) {
|
|
10
|
+
return -1;
|
|
11
|
+
} else if (a[firstKey] < b[firstKey]) {
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
if (a[secondKey] > b[secondKey]) {
|
|
16
|
+
return 1;
|
|
17
|
+
} else if (a[secondKey] < b[secondKey]) {
|
|
18
|
+
return -1;
|
|
19
|
+
} else {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = rankingSorter;
|
package/src/js/sleep.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep for a given amount of time.
|
|
3
|
+
* @param ms The time to sleep in milliseconds
|
|
4
|
+
* @returns {Promise<unknown>} A promise that resolves after the given time
|
|
5
|
+
*/
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = sleep;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
test,
|
|
5
|
+
} from '@jest/globals';
|
|
6
|
+
|
|
7
|
+
const levenshteinDistance = require('../src/js/levenshtein-distance');
|
|
8
|
+
|
|
9
|
+
describe('levenshteinDistance', () => {
|
|
10
|
+
const testCases = [
|
|
11
|
+
{ a: 'test', b: 'test', expected: 100 },
|
|
12
|
+
{ a: 'abc', b: 'xyz', expected: 0 },
|
|
13
|
+
{ a: 'kitten', b: 'sitting', expected: 57.14 },
|
|
14
|
+
{ a: 'flaw', b: 'lawn', expected: 50 },
|
|
15
|
+
{ a: '', b: 'nonempty', expected: 8 },
|
|
16
|
+
{ a: 'nonempty', b: '', expected: 8 }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
test.each(testCases)(
|
|
20
|
+
'should return $expected when comparing "$a" and "$b"',
|
|
21
|
+
({ a, b, expected }) => {
|
|
22
|
+
const result = levenshteinDistance(a, b);
|
|
23
|
+
expect(result).toBeCloseTo(expected, 2);
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
beforeEach,
|
|
3
|
+
describe,
|
|
4
|
+
expect,
|
|
5
|
+
it,
|
|
6
|
+
} from '@jest/globals';
|
|
7
|
+
|
|
8
|
+
const loadScript = require('../src/js/load-script');
|
|
9
|
+
|
|
10
|
+
describe('loadScript', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
document.head.innerHTML = '';
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should load a script and call the callback on success', (done) => {
|
|
16
|
+
const url = 'https://example.com/test-script.js';
|
|
17
|
+
loadScript(url, (err, script) => {
|
|
18
|
+
expect(err).toBeNull();
|
|
19
|
+
expect(script).toBeInstanceOf(HTMLScriptElement);
|
|
20
|
+
expect(script.src).toBe(url);
|
|
21
|
+
done();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const script = document.head.querySelector('script');
|
|
25
|
+
script.onload();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should call the callback with an error on failure', (done) => {
|
|
29
|
+
const url = 'https://example.com/test-script.js';
|
|
30
|
+
loadScript(url, (err) => {
|
|
31
|
+
expect(err).toBeInstanceOf(Error);
|
|
32
|
+
expect(err.message).toBe(`Failed to load script: ${url}`);
|
|
33
|
+
done();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const script = document.head.querySelector('script');
|
|
37
|
+
script.onerror();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should load a script without a callback', () => {
|
|
41
|
+
const url = 'https://example.com/test-script.js';
|
|
42
|
+
loadScript(url);
|
|
43
|
+
|
|
44
|
+
const script = document.head.querySelector('script');
|
|
45
|
+
expect(script).toBeInstanceOf(HTMLScriptElement);
|
|
46
|
+
expect(script.src).toBe(url);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
it,
|
|
7
|
+
jest,
|
|
8
|
+
} from '@jest/globals';
|
|
9
|
+
|
|
10
|
+
const { fetchRandomQuote, resetQuoteCache } = require('../src/js/random-quote');
|
|
11
|
+
|
|
12
|
+
describe('fetchRandomQuote', () => {
|
|
13
|
+
let originalFetch;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Save the original fetch function
|
|
17
|
+
originalFetch = globalThis.fetch;
|
|
18
|
+
// Clear the quote cache before each test
|
|
19
|
+
jest.resetModules();
|
|
20
|
+
// Reset the quotesCache
|
|
21
|
+
resetQuoteCache();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
// Restore the original fetch function
|
|
26
|
+
globalThis.fetch = originalFetch;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should fetch and return a random quote', async () => {
|
|
30
|
+
const mockQuotes = [
|
|
31
|
+
{ quote: 'Quote 1' },
|
|
32
|
+
{ quote: 'Quote 2' },
|
|
33
|
+
{ quote: 'Quote 3' }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
globalThis.fetch = jest.fn(() =>
|
|
37
|
+
Promise.resolve({
|
|
38
|
+
json: () => Promise.resolve(mockQuotes)
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const quote = await fetchRandomQuote();
|
|
43
|
+
expect(mockQuotes).toContainEqual(quote);
|
|
44
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return a cached quote if already fetched', async () => {
|
|
48
|
+
const mockQuotes = [
|
|
49
|
+
{ quote: 'Quote 1' },
|
|
50
|
+
{ quote: 'Quote 2' },
|
|
51
|
+
{ quote: 'Quote 3' }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
globalThis.fetch = jest.fn(() =>
|
|
55
|
+
Promise.resolve({
|
|
56
|
+
json: () => Promise.resolve(mockQuotes)
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// First call to populate the cache
|
|
61
|
+
await fetchRandomQuote();
|
|
62
|
+
// Second call should use the cache
|
|
63
|
+
const quote = await fetchRandomQuote();
|
|
64
|
+
|
|
65
|
+
expect(mockQuotes).toContainEqual(quote);
|
|
66
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle fetch errors gracefully', async () => {
|
|
70
|
+
globalThis.fetch = jest.fn(() =>
|
|
71
|
+
Promise.reject(new Error('Failed to fetch'))
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await expect(fetchRandomQuote()).rejects.toThrow('Failed to fetch');
|
|
75
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
it,
|
|
5
|
+
} from '@jest/globals';
|
|
6
|
+
|
|
7
|
+
const rankingSorter = require('../src/js/ranking-sorter');
|
|
8
|
+
|
|
9
|
+
describe('rankingSorter', () => {
|
|
10
|
+
const testCases = [
|
|
11
|
+
{
|
|
12
|
+
description: 'should sort by firstKey descending and secondKey ascending',
|
|
13
|
+
data: [
|
|
14
|
+
{ key1: 1, key2: 2 },
|
|
15
|
+
{ key1: 2, key2: 1 },
|
|
16
|
+
{ key1: 1, key2: 1 }
|
|
17
|
+
],
|
|
18
|
+
expected: [
|
|
19
|
+
{ key1: 2, key2: 1 },
|
|
20
|
+
{ key1: 1, key2: 1 },
|
|
21
|
+
{ key1: 1, key2: 2 }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
description: 'should return 1 when firstKey values are equal and secondKey of first object is greater',
|
|
26
|
+
data: [
|
|
27
|
+
{ key1: 1, key2: 2 },
|
|
28
|
+
{ key1: 1, key2: 1 }
|
|
29
|
+
],
|
|
30
|
+
expected: 1,
|
|
31
|
+
compare: true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
description: 'should return 0 when both firstKey and secondKey values are equal',
|
|
35
|
+
data: [
|
|
36
|
+
{ key1: 1, key2: 1 },
|
|
37
|
+
{ key1: 1, key2: 1 }
|
|
38
|
+
],
|
|
39
|
+
expected: 0,
|
|
40
|
+
compare: true
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
testCases.forEach(({ description, data, expected, compare }) => {
|
|
45
|
+
it(description, () => {
|
|
46
|
+
if (compare) {
|
|
47
|
+
const result = rankingSorter('key1', 'key2')(data[0], data[1]);
|
|
48
|
+
expect(result).toBe(expected);
|
|
49
|
+
} else {
|
|
50
|
+
const sorted = data.sort(rankingSorter('key1', 'key2'));
|
|
51
|
+
expect(sorted).toEqual(expected);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
beforeAll,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from '@jest/globals';
|
|
10
|
+
|
|
11
|
+
const sleep = require('../src/js/sleep');
|
|
12
|
+
|
|
13
|
+
describe('sleep function', () => {
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
jest.useFakeTimers();
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.spyOn(global, 'setTimeout');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test.each([
|
|
23
|
+
[500], // 0.5 second
|
|
24
|
+
[1000], // 1 second
|
|
25
|
+
[1500], // 1.5 seconds
|
|
26
|
+
[2000], // 2 seconds
|
|
27
|
+
[60000], // 60 seconds
|
|
28
|
+
])('resolves after %i milliseconds', async (delay) => {
|
|
29
|
+
let delay_value = delay[0];
|
|
30
|
+
const sleepPromise = sleep(delay_value);
|
|
31
|
+
jest.advanceTimersByTime(delay_value);
|
|
32
|
+
await sleepPromise;
|
|
33
|
+
|
|
34
|
+
// Allow a small margin of error for the timer
|
|
35
|
+
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), delay_value);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterAll(() => {
|
|
39
|
+
jest.useRealTimers();
|
|
40
|
+
});
|
|
41
|
+
});
|