mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Add type checking and JSDoc comments to some utils
This commit is contained in:
@ -7,6 +7,14 @@ export function onlyUnique(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string only contains digits.
|
||||
* @param {string} str The string to check.
|
||||
* @returns {boolean} True if the string only contains digits, false otherwise.
|
||||
* @example
|
||||
* isDigitsOnly('123'); // true
|
||||
* isDigitsOnly('abc'); // false
|
||||
*/
|
||||
export function isDigitsOnly(str) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
@ -16,6 +24,13 @@ export function getSortableDelay() {
|
||||
return navigator.maxTouchPoints > 0 ? 750 : 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rearranges an array in a random order.
|
||||
* @param {any[]} array The array to shuffle.
|
||||
* @returns {any[]} The shuffled array.
|
||||
* @example
|
||||
* shuffle([1, 2, 3]); // [2, 3, 1]
|
||||
*/
|
||||
export function shuffle(array) {
|
||||
let currentIndex = array.length,
|
||||
randomIndex;
|
||||
@ -31,6 +46,12 @@ export function shuffle(array) {
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file to the user's devices.
|
||||
* @param {BlobPart} content File content to download.
|
||||
* @param {string} fileName File name.
|
||||
* @param {string} contentType File content type.
|
||||
*/
|
||||
export function download(content, fileName, contentType) {
|
||||
const a = document.createElement("a");
|
||||
const file = new Blob([content], { type: contentType });
|
||||
@ -49,12 +70,17 @@ export async function urlContentToDataUri(url, params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the file's text.
|
||||
* @param {Blob} file The file to read.
|
||||
* @returns {Promise<string>} A promise that resolves to the file's text.
|
||||
*/
|
||||
export function getFileText(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
reader.onload = function () {
|
||||
resolve(reader.result);
|
||||
resolve(String(reader.result));
|
||||
};
|
||||
reader.onerror = function (error) {
|
||||
reject(error);
|
||||
@ -62,6 +88,10 @@ export function getFileText(file) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the file's array buffer.
|
||||
* @param {Blob} file The file to read.
|
||||
*/
|
||||
export function getFileBuffer(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@ -75,12 +105,17 @@ export function getFileBuffer(file) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the base64 encoded string of a file.
|
||||
* @param {Blob} file The file to read.
|
||||
* @returns {Promise<string>} A promise that resolves to the base64 encoded string.
|
||||
*/
|
||||
export function getBase64Async(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function () {
|
||||
resolve(reader.result);
|
||||
resolve(String(reader.result));
|
||||
};
|
||||
reader.onerror = function (error) {
|
||||
reject(error);
|
||||
@ -88,15 +123,26 @@ export function getBase64Async(file) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a file blob as a JSON object.
|
||||
* @param {Blob} file The file to read.
|
||||
* @returns {Promise<any>} A promise that resolves to the parsed JSON object.
|
||||
*/
|
||||
export async function parseJsonFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => resolve(JSON.parse(event.target.result));
|
||||
fileReader.onerror = error => reject(error);
|
||||
fileReader.readAsText(file);
|
||||
fileReader.onload = event => resolve(JSON.parse(String(event.target.result)));
|
||||
fileReader.onerror = error => reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a hash code for a string.
|
||||
* @param {string} str The string to hash.
|
||||
* @param {number} [seed=0] The seed to use for the hash.
|
||||
* @returns {number} The hash code.
|
||||
*/
|
||||
export function getStringHash(str, seed = 0) {
|
||||
if (typeof str !== 'string') {
|
||||
return 0;
|
||||
@ -116,6 +162,12 @@ export function getStringHash(str, seed = 0) {
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
|
||||
* @param {function} func The function to debounce.
|
||||
* @param {number} [timeout=300] The timeout in milliseconds.
|
||||
* @returns {function} The debounced function.
|
||||
*/
|
||||
export function debounce(func, timeout = 300) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
@ -124,6 +176,12 @@ export function debounce(func, timeout = 300) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a throttled function that only invokes func at most once per every limit milliseconds.
|
||||
* @param {function} func The function to throttle.
|
||||
* @param {number} [limit=300] The limit in milliseconds.
|
||||
* @returns {function} The throttled function.
|
||||
*/
|
||||
export function throttle(func, limit = 300) {
|
||||
let lastCall;
|
||||
return (...args) => {
|
||||
@ -135,6 +193,11 @@ export function throttle(func, limit = 300) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an element is in the viewport.
|
||||
* @param {any[]} el The element to check.
|
||||
* @returns {boolean} True if the element is in the viewport, false otherwise.
|
||||
*/
|
||||
export function isElementInViewport(el) {
|
||||
if (typeof jQuery === "function" && el instanceof jQuery) {
|
||||
el = el[0];
|
||||
@ -148,6 +211,12 @@ export function isElementInViewport(el) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a name that is unique among the names that exist.
|
||||
* @param {string} name The name to check.
|
||||
* @param {{ (y: any): boolean; }} exists Function to check if name exists.
|
||||
* @returns {string} A unique name.
|
||||
*/
|
||||
export function getUniqueName(name, exists) {
|
||||
let i = 1;
|
||||
let baseName = name;
|
||||
@ -158,18 +227,48 @@ export function getUniqueName(name, exists) {
|
||||
return name;
|
||||
}
|
||||
|
||||
export const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||
export const isSubsetOf = (a, b) => (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false;
|
||||
/**
|
||||
* Returns a promise that resolves after the specified number of milliseconds.
|
||||
* @param {number} ms The number of milliseconds to wait.
|
||||
* @returns {Promise<void>} A promise that resolves after the specified number of milliseconds.
|
||||
*/
|
||||
export function delay(ms) {
|
||||
return new Promise((res) => setTimeout(res, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an array is a subset of another array.
|
||||
* @param {any[]} a Array A
|
||||
* @param {any[]} b Array B
|
||||
* @returns {boolean} True if B is a subset of A, false otherwise.
|
||||
*/
|
||||
export function isSubsetOf(a, b) {
|
||||
return (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the trailing number in a string.
|
||||
* @param {string} str The string to process.
|
||||
* @returns {string} The string with the trailing number incremented by 1.
|
||||
* @example
|
||||
* incrementString('Hello, world! 1'); // 'Hello, world! 2'
|
||||
*/
|
||||
export function incrementString(str) {
|
||||
// Find the trailing number or it will match the empty string
|
||||
const count = str.match(/\d*$/);
|
||||
|
||||
// Take the substring up until where the integer was matched
|
||||
// Concatenate it to the matched count incremented by 1
|
||||
return str.substr(0, count.index) + (++count[0]);
|
||||
return str.substring(0, count.index) + (Number(count[0]) + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a string using the specified arguments.
|
||||
* @param {string} format The format string.
|
||||
* @returns {string} The formatted string.
|
||||
* @example
|
||||
* stringFormat('Hello, {0}!', 'world'); // 'Hello, world!'
|
||||
*/
|
||||
export function stringFormat(format) {
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
return format.replace(/{(\d+)}/g, function (match, number) {
|
||||
@ -180,7 +279,11 @@ export function stringFormat(format) {
|
||||
});
|
||||
};
|
||||
|
||||
// Save the caret position in a contenteditable element
|
||||
/**
|
||||
* Save the caret position in a contenteditable element.
|
||||
* @param {Element} element The element to save the caret position of.
|
||||
* @returns {{ start: number, end: number }} An object with the start and end offsets of the caret.
|
||||
*/
|
||||
export function saveCaretPosition(element) {
|
||||
// Get the current selection
|
||||
const selection = window.getSelection();
|
||||
@ -209,7 +312,11 @@ export function saveCaretPosition(element) {
|
||||
return position;
|
||||
}
|
||||
|
||||
// Restore the caret position in a contenteditable element
|
||||
/**
|
||||
* Restore the caret position in a contenteditable element.
|
||||
* @param {Element} element The element to restore the caret position of.
|
||||
* @param {{ start: any; end: any; }} position An object with the start and end offsets of the caret.
|
||||
*/
|
||||
export function restoreCaretPosition(element, position) {
|
||||
// If the position is null, do nothing
|
||||
if (!position) {
|
||||
@ -236,6 +343,11 @@ export async function resetScrollHeight(element) {
|
||||
$(element).css('height', $(element).prop('scrollHeight') + 3 + 'px');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of an element to its scroll height.
|
||||
* @param {JQuery<HTMLElement>} element The element to initialize the scroll height of.
|
||||
* @returns {Promise<void>} A promise that resolves when the scroll height has been initialized.
|
||||
*/
|
||||
export async function initScrollHeight(element) {
|
||||
await delay(1);
|
||||
|
||||
@ -252,15 +364,27 @@ export async function initScrollHeight(element) {
|
||||
//resetScrollHeight(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares elements by their CSS order property. Used for sorting.
|
||||
* @param {any} a The first element.
|
||||
* @param {any} b The second element.
|
||||
* @returns {number} A negative number if a is before b, a positive number if a is after b, or 0 if they are equal.
|
||||
*/
|
||||
export function sortByCssOrder(a, b) {
|
||||
const _a = Number($(a).css('order'));
|
||||
const _b = Number($(b).css('order'));
|
||||
return _a - _b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims a string to the end of a nearest sentence.
|
||||
* @param {string} input The string to trim.
|
||||
* @param {boolean} include_newline Whether to include a newline character in the trimmed string.
|
||||
* @returns {string} The trimmed string.
|
||||
* @example
|
||||
* end_trim_to_sentence('Hello, world! I am from'); // 'Hello, world!'
|
||||
*/
|
||||
export function end_trim_to_sentence(input, include_newline = false) {
|
||||
// inspired from https://github.com/kaihordewebui/kaihordewebui.github.io/blob/06b95e6b7720eb85177fbaf1a7f52955d7cdbc02/index.html#L4853-L4867
|
||||
|
||||
const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '!', '?', '”', ')', '】', '】', '’', '」', '】']); // extend this as you see fit
|
||||
let last = -1;
|
||||
|
||||
@ -285,6 +409,15 @@ export function end_trim_to_sentence(input, include_newline = false) {
|
||||
return input.substring(0, last + 1).trimEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of occurrences of a character in a string.
|
||||
* @param {string} string The string to count occurrences in.
|
||||
* @param {string} character The character to count occurrences of.
|
||||
* @returns {number} The number of occurrences of the character in the string.
|
||||
* @example
|
||||
* countOccurrences('Hello, world!', 'l'); // 3
|
||||
* countOccurrences('Hello, world!', 'x'); // 0
|
||||
*/
|
||||
export function countOccurrences(string, character) {
|
||||
let count = 0;
|
||||
|
||||
@ -297,6 +430,14 @@ export function countOccurrences(string, character) {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a number is odd.
|
||||
* @param {number} number The number to check.
|
||||
* @returns {boolean} True if the number is odd, false otherwise.
|
||||
* @example
|
||||
* isOdd(3); // true
|
||||
* isOdd(4); // false
|
||||
*/
|
||||
export function isOdd(number) {
|
||||
return number % 2 !== 0;
|
||||
}
|
||||
@ -337,6 +478,12 @@ export function timestampToMoment(timestamp) {
|
||||
return moment.invalid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two moment objects for sorting.
|
||||
* @param {*} a The first moment object.
|
||||
* @param {*} b The second moment object.
|
||||
* @returns {number} A negative number if a is before b, a positive number if a is after b, or 0 if they are equal.
|
||||
*/
|
||||
export function sortMoments(a, b) {
|
||||
if (a.isBefore(b)) {
|
||||
return 1;
|
||||
@ -347,14 +494,21 @@ export function sortMoments(a, b) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Split string to parts no more than length in size */
|
||||
export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ', '']) {
|
||||
const delim = delimitiers[0] ?? '';
|
||||
/** Split string to parts no more than length in size.
|
||||
* @param {string} input The string to split.
|
||||
* @param {number} length The maximum length of each part.
|
||||
* @param {string[]} delimiters The delimiters to use when splitting the string.
|
||||
* @returns {string[]} The split string.
|
||||
* @example
|
||||
* splitRecursive('Hello, world!', 3); // ['Hel', 'lo,', 'wor', 'ld!']
|
||||
*/
|
||||
export function splitRecursive(input, length, delimiters = ['\n\n', '\n', ' ', '']) {
|
||||
const delim = delimiters[0] ?? '';
|
||||
const parts = input.split(delim);
|
||||
|
||||
const flatParts = parts.flatMap(p => {
|
||||
if (p.length < length) return p;
|
||||
return splitRecursive(input, length, delimitiers.slice(1));
|
||||
return splitRecursive(input, length, delimiters.slice(1));
|
||||
});
|
||||
|
||||
// Merge short chunks
|
||||
@ -378,6 +532,13 @@ export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ',
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid data URL.
|
||||
* @param {string} str The string to check.
|
||||
* @returns {boolean} True if the string is a valid data URL, false otherwise.
|
||||
* @example
|
||||
* isDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...'); // true
|
||||
*/
|
||||
export function isDataURL(str) {
|
||||
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i;
|
||||
return regex.test(str);
|
||||
@ -392,6 +553,13 @@ export function getCharaFilename(chid) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts words from a string.
|
||||
* @param {string} value The string to extract words from.
|
||||
* @returns {string[]} The extracted words.
|
||||
* @example
|
||||
* extractAllWords('Hello, world!'); // ['hello', 'world']
|
||||
*/
|
||||
export function extractAllWords(value) {
|
||||
const words = [];
|
||||
|
||||
@ -406,21 +574,45 @@ export function extractAllWords(value) {
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for use in a regular expression.
|
||||
* @param {string} string The string to escape.
|
||||
* @returns {string} The escaped string.
|
||||
* @example
|
||||
* escapeRegex('^Hello$'); // '\\^Hello\\$'
|
||||
*/
|
||||
export function escapeRegex(string) {
|
||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an interface for rate limiting function calls.
|
||||
*/
|
||||
export class RateLimiter {
|
||||
constructor(intervalMillis) {
|
||||
this._intervalMillis = intervalMillis;
|
||||
this._lastResolveTime = 0;
|
||||
this._pendingResolve = Promise.resolve();
|
||||
/**
|
||||
* Creates a new RateLimiter.
|
||||
* @param {number} interval The interval in milliseconds.
|
||||
* @example
|
||||
* const rateLimiter = new RateLimiter(1000);
|
||||
* rateLimiter.waitForResolve().then(() => {
|
||||
* console.log('Waited 1000ms');
|
||||
* });
|
||||
*/
|
||||
constructor(interval) {
|
||||
this.interval = interval;
|
||||
this.lastResolveTime = 0;
|
||||
this.pendingResolve = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the remaining time in the interval.
|
||||
* @param {AbortSignal} abortSignal An optional AbortSignal to abort the wait.
|
||||
* @returns {Promise<void>} A promise that resolves when the remaining time has elapsed.
|
||||
*/
|
||||
_waitRemainingTime(abortSignal) {
|
||||
const currentTime = Date.now();
|
||||
const elapsedTime = currentTime - this._lastResolveTime;
|
||||
const remainingTime = Math.max(0, this._intervalMillis - elapsedTime);
|
||||
const elapsedTime = currentTime - this.lastResolveTime;
|
||||
const remainingTime = Math.max(0, this.interval - elapsedTime);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@ -436,19 +628,29 @@ export class RateLimiter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the next interval to elapse.
|
||||
* @param {AbortSignal} abortSignal An optional AbortSignal to abort the wait.
|
||||
* @returns {Promise<void>} A promise that resolves when the next interval has elapsed.
|
||||
*/
|
||||
async waitForResolve(abortSignal) {
|
||||
await this._pendingResolve;
|
||||
this._pendingResolve = this._waitRemainingTime(abortSignal);
|
||||
await this.pendingResolve;
|
||||
this.pendingResolve = this._waitRemainingTime(abortSignal);
|
||||
|
||||
// Update the last resolve time
|
||||
this._lastResolveTime = Date.now() + this._intervalMillis;
|
||||
console.debug(`RateLimiter.waitForResolve() ${this._lastResolveTime}`);
|
||||
this.lastResolveTime = Date.now() + this.interval;
|
||||
console.debug(`RateLimiter.waitForResolve() ${this.lastResolveTime}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from https://github.com/LostRuins/lite.koboldai.net/blob/main/index.html
|
||||
//import tavern png data. adapted from png-chunks-extract under MIT license
|
||||
//accepts png input data, and returns the extracted JSON
|
||||
/**
|
||||
* Extracts a JSON object from a PNG file.
|
||||
* Taken from https://github.com/LostRuins/lite.koboldai.net/blob/main/index.html
|
||||
* Adapted from png-chunks-extract under MIT license
|
||||
* @param {Uint8Array} data The PNG data to extract the JSON from.
|
||||
* @param {string} identifier The identifier to look for in the PNG tEXT data.
|
||||
* @returns {object} The extracted JSON object.
|
||||
*/
|
||||
export function extractDataFromPng(data, identifier = 'chara') {
|
||||
console.log("Attempting PNG import...");
|
||||
let uint8 = new Uint8Array(4);
|
||||
@ -599,6 +801,13 @@ export async function saveBase64AsFile(base64Data, characterName, filename = "",
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a thumbnail from a data URL.
|
||||
* @param {string} dataUrl The data URL encoded data of the image.
|
||||
* @param {number} maxWidth The maximum width of the thumbnail.
|
||||
* @param {number} maxHeight The maximum height of the thumbnail.
|
||||
* @returns {Promise<string>} A promise that resolves to the thumbnail data URL.
|
||||
*/
|
||||
export function createThumbnail(dataUrl, maxWidth, maxHeight) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
@ -634,6 +843,13 @@ export function createThumbnail(dataUrl, maxWidth, maxHeight) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a condition to be true. Throws an error if the condition is not true within the timeout.
|
||||
* @param {{ (): boolean; }} condition The condition to wait for.
|
||||
* @param {number} [timeout=1000] The timeout in milliseconds.
|
||||
* @param {number} [interval=100] The interval in milliseconds.
|
||||
* @returns {Promise<void>} A promise that resolves when the condition is true.
|
||||
*/
|
||||
export async function waitUntilCondition(condition, timeout = 1000, interval = 100) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@ -651,6 +867,12 @@ export async function waitUntilCondition(condition, timeout = 1000, interval = 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a UUID v4 string.
|
||||
* @returns {string} A UUID v4 string.
|
||||
* @example
|
||||
* uuidv4(); // '3e2fd9e1-0a7a-4f6d-9aaf-8a7a4babe7eb'
|
||||
*/
|
||||
export function uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
@ -659,6 +881,11 @@ export function uuidv4() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an object using JSON serialization.
|
||||
* @param {any} obj The object to clone.
|
||||
* @returns {any} A deep clone of the object.
|
||||
*/
|
||||
export function deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
Reference in New Issue
Block a user