2024-10-17 01:02:45 +02:00
import {
moment ,
DOMPurify ,
Readability ,
isProbablyReaderable ,
} from '../lib.js' ;
2024-10-16 22:11:13 +02:00
2023-12-02 19:04:51 +01:00
import { getContext } from './extensions.js' ;
2024-09-29 03:20:01 +02:00
import { characters , getRequestHeaders , this _chid } from '../script.js' ;
2023-12-02 19:04:51 +01:00
import { isMobile } from './RossAscends-mods.js' ;
import { collapseNewlines } from './power-user.js' ;
2024-04-28 18:47:53 +02:00
import { debounce _timeout } from './constants.js' ;
2024-07-09 19:28:06 +02:00
import { Popup , POPUP _RESULT , POPUP _TYPE } from './popup.js' ;
2024-08-29 14:55:54 +02:00
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js' ;
2024-09-29 03:20:01 +02:00
import { getTagsList } from './tags.js' ;
import { groups , selected _group } from './group-chats.js' ;
2024-10-28 10:01:48 +01:00
import { getCurrentLocale } from './i18n.js' ;
2023-07-20 19:32:15 +02:00
2023-08-22 21:45:12 +02:00
/ * *
* Pagination status string template .
* @ type { string }
* /
2023-08-21 20:10:11 +02:00
export const PAGINATION _TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>' ;
2023-08-22 21:45:12 +02:00
/ * *
* Navigation options for pagination .
* @ enum { number }
* /
2023-12-02 21:06:57 +01:00
export const navigation _option = {
none : - 2000 ,
previous : - 1000 ,
} ;
2023-08-22 21:45:12 +02:00
2024-10-17 21:45:33 +02:00
/ * *
* Determines if a value is an object .
* @ param { any } item The item to check .
* @ returns { boolean } True if the item is an object , false otherwise .
* /
function isObject ( item ) {
return ( item && typeof item === 'object' && ! Array . isArray ( item ) ) ;
}
/ * *
* Merges properties of two objects . If the property is an object , it will be merged recursively .
* @ param { object } target The target object
* @ param { object } source The source object
* @ returns { object } Merged object
* /
export function deepMerge ( target , source ) {
let output = Object . assign ( { } , target ) ;
if ( isObject ( target ) && isObject ( source ) ) {
Object . keys ( source ) . forEach ( key => {
if ( isObject ( source [ key ] ) ) {
if ( ! ( key in target ) )
Object . assign ( output , { [ key ] : source [ key ] } ) ;
else
output [ key ] = deepMerge ( target [ key ] , source [ key ] ) ;
} else {
Object . assign ( output , { [ key ] : source [ key ] } ) ;
}
} ) ;
}
return output ;
}
2023-08-30 11:03:18 +02:00
export function escapeHtml ( str ) {
return String ( str ) . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) . replace ( /"/g , '"' ) ;
}
2023-10-15 23:50:29 +02:00
export function isValidUrl ( value ) {
2023-09-08 21:44:06 +02:00
try {
new URL ( value ) ;
return true ;
} catch ( _ ) {
return false ;
}
}
2024-08-29 14:55:54 +02:00
/ * *
* Converts string to a value of a given type . Includes pythonista - friendly aliases .
* @ param { string | SlashCommandClosure } value String value
* @ param { string } type Type to convert to
* @ returns { any } Converted value
* /
export function convertValueType ( value , type ) {
if ( value instanceof SlashCommandClosure || typeof type !== 'string' ) {
return value ;
}
2024-09-01 22:25:29 +02:00
switch ( type . trim ( ) . toLowerCase ( ) ) {
2024-08-29 14:55:54 +02:00
case 'string' :
case 'str' :
return String ( value ) ;
case 'null' :
return null ;
case 'undefined' :
case 'none' :
return undefined ;
case 'number' :
return Number ( value ) ;
case 'int' :
return parseInt ( value , 10 ) ;
case 'float' :
return parseFloat ( value ) ;
case 'boolean' :
case 'bool' :
return isTrueBoolean ( value ) ;
case 'list' :
case 'array' :
try {
const parsedArray = JSON . parse ( value ) ;
if ( Array . isArray ( parsedArray ) ) {
return parsedArray ;
}
2024-09-01 10:44:56 +02:00
// The value is not an array
return [ ] ;
2024-08-29 14:55:54 +02:00
} catch {
return [ ] ;
}
case 'object' :
case 'dict' :
case 'dictionary' :
try {
const parsedObject = JSON . parse ( value ) ;
if ( typeof parsedObject === 'object' ) {
return parsedObject ;
}
2024-09-01 10:44:56 +02:00
// The value is not an object
return { } ;
2024-08-29 14:55:54 +02:00
} catch {
return { } ;
}
default :
return value ;
}
}
2023-11-08 22:04:32 +01:00
/ * *
* Parses ranges like 10 - 20 or 10.
* Range is inclusive . Start must be less than end .
* Returns null if invalid .
* @ param { string } input The input string .
* @ param { number } min The minimum value .
* @ param { number } max The maximum value .
* @ returns { { start : number , end : number } } The parsed range .
* /
export function stringToRange ( input , min , max ) {
let start , end ;
2024-03-15 15:31:43 +01:00
if ( typeof input !== 'string' ) {
input = String ( input ) ;
}
2023-11-08 22:04:32 +01:00
if ( input . includes ( '-' ) ) {
const parts = input . split ( '-' ) ;
start = parts [ 0 ] ? parseInt ( parts [ 0 ] , 10 ) : NaN ;
end = parts [ 1 ] ? parseInt ( parts [ 1 ] , 10 ) : NaN ;
} else {
start = end = parseInt ( input , 10 ) ;
}
if ( isNaN ( start ) || isNaN ( end ) || start > end || start < min || end > max ) {
return null ;
}
return { start , end } ;
}
2023-08-22 21:45:12 +02:00
/ * *
* Determines if a value is unique in an array .
* @ param { any } value Current value .
* @ param { number } index Current index .
* @ param { any } array The array being processed .
* @ returns { boolean } True if the value is unique , false otherwise .
* /
2023-07-20 19:32:15 +02:00
export function onlyUnique ( value , index , array ) {
return array . indexOf ( value ) === index ;
}
2024-05-27 03:35:03 +02:00
/ * *
* Removes the first occurrence of a specified item from an array
*
* @ param { * [ ] } array - The array from which to remove the item
* @ param { * } item - The item to remove from the array
* @ returns { boolean } - Returns true if the item was successfully removed , false otherwise .
* /
export function removeFromArray ( array , item ) {
const index = array . indexOf ( item ) ;
if ( index === - 1 ) return false ;
array . splice ( index , 1 ) ;
return true ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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
* /
2023-08-08 21:36:42 +02:00
export function isDigitsOnly ( str ) {
return /^\d+$/ . test ( str ) ;
}
2023-08-22 21:45:12 +02:00
/ * *
* Gets a drag delay for sortable elements . This is to prevent accidental drags when scrolling .
2023-08-24 22:52:03 +02:00
* @ returns { number } The delay in milliseconds . 50 ms for desktop , 750 ms for mobile .
2023-08-22 21:45:12 +02:00
* /
2023-08-18 12:41:46 +02:00
export function getSortableDelay ( ) {
2023-08-24 22:52:03 +02:00
return isMobile ( ) ? 750 : 50 ;
2023-08-18 12:41:46 +02:00
}
2023-08-27 17:27:34 +02:00
export async function bufferToBase64 ( buffer ) {
// use a FileReader to generate a base64 data URI:
const base64url = await new Promise ( resolve => {
2023-12-02 20:11:06 +01:00
const reader = new FileReader ( ) ;
reader . onload = ( ) => resolve ( reader . result ) ;
reader . readAsDataURL ( new Blob ( [ buffer ] ) ) ;
2023-08-27 17:27:34 +02:00
} ) ;
// remove the `data:...;base64,` part from the start
return base64url . slice ( base64url . indexOf ( ',' ) + 1 ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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]
* /
2023-07-20 19:32:15 +02:00
export function shuffle ( array ) {
let currentIndex = array . length ,
randomIndex ;
while ( currentIndex != 0 ) {
randomIndex = Math . floor ( Math . random ( ) * currentIndex ) ;
currentIndex -- ;
[ array [ currentIndex ] , array [ randomIndex ] ] = [
array [ randomIndex ] ,
array [ currentIndex ] ,
] ;
}
return array ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function download ( content , fileName , contentType ) {
2023-12-02 19:04:51 +01:00
const a = document . createElement ( 'a' ) ;
2023-07-20 19:32:15 +02:00
const file = new Blob ( [ content ] , { type : contentType } ) ;
a . href = URL . createObjectURL ( file ) ;
a . download = fileName ;
a . click ( ) ;
2024-05-22 00:36:38 +02:00
URL . revokeObjectURL ( a . href ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-22 21:45:12 +02:00
/ * *
* Fetches a file by URL and parses its contents as data URI .
* @ param { string } url The URL to fetch .
* @ param { any } params Fetch parameters .
* @ returns { Promise < string > } A promise that resolves to the data URI .
* /
2023-07-20 19:32:15 +02:00
export async function urlContentToDataUri ( url , params ) {
const response = await fetch ( url , params ) ;
const blob = await response . blob ( ) ;
2023-08-22 21:45:12 +02:00
return await new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . onload = function ( ) {
resolve ( String ( reader . result ) ) ;
} ;
reader . onerror = function ( error ) {
reject ( error ) ;
} ;
2023-07-20 19:32:15 +02:00
reader . readAsDataURL ( blob ) ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function getFileText ( file ) {
return new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . readAsText ( file ) ;
reader . onload = function ( ) {
2023-08-22 12:07:24 +02:00
resolve ( String ( reader . result ) ) ;
2023-07-20 19:32:15 +02:00
} ;
reader . onerror = function ( error ) {
reject ( error ) ;
} ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Returns a promise that resolves to the file ' s array buffer .
* @ param { Blob } file The file to read .
* /
2023-07-20 19:32:15 +02:00
export function getFileBuffer ( file ) {
return new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . readAsArrayBuffer ( file ) ;
reader . onload = function ( ) {
resolve ( reader . result ) ;
} ;
reader . onerror = function ( error ) {
reject ( error ) ;
} ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function getBase64Async ( file ) {
return new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . readAsDataURL ( file ) ;
reader . onload = function ( ) {
2023-08-22 12:07:24 +02:00
resolve ( String ( reader . result ) ) ;
2023-07-20 19:32:15 +02:00
} ;
reader . onerror = function ( error ) {
reject ( error ) ;
} ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export async function parseJsonFile ( file ) {
return new Promise ( ( resolve , reject ) => {
const fileReader = new FileReader ( ) ;
fileReader . readAsText ( file ) ;
2023-08-22 12:07:24 +02:00
fileReader . onload = event => resolve ( JSON . parse ( String ( event . target . result ) ) ) ;
fileReader . onerror = error => reject ( error ) ;
2023-07-20 19:32:15 +02:00
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function getStringHash ( str , seed = 0 ) {
if ( typeof str !== 'string' ) {
return 0 ;
}
let h1 = 0xdeadbeef ^ seed ,
h2 = 0x41c6ce57 ^ seed ;
for ( let i = 0 , ch ; i < str . length ; i ++ ) {
ch = str . charCodeAt ( i ) ;
h1 = Math . imul ( h1 ^ ch , 2654435761 ) ;
h2 = Math . imul ( h2 ^ ch , 1597334677 ) ;
}
h1 = Math . imul ( h1 ^ ( h1 >>> 16 ) , 2246822507 ) ^ Math . imul ( h2 ^ ( h2 >>> 13 ) , 3266489909 ) ;
h2 = Math . imul ( h2 ^ ( h2 >>> 16 ) , 2246822507 ) ^ Math . imul ( h1 ^ ( h1 >>> 13 ) , 3266489909 ) ;
return 4294967296 * ( 2097151 & h2 ) + ( h1 >>> 0 ) ;
2023-12-02 16:15:03 +01:00
}
2023-07-20 19:32:15 +02:00
2024-07-22 21:20:03 +02:00
/ * *
* Map of debounced functions to their timers .
* Weak map is used to avoid memory leaks .
* @ type { WeakMap < function , any > }
* /
const debounceMap = new WeakMap ( ) ;
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
2024-04-28 06:21:47 +02:00
* @ param { debounce _timeout | number } [ timeout = debounce _timeout . default ] The timeout based on the common enum values , or in milliseconds .
2023-08-22 12:07:24 +02:00
* @ returns { function } The debounced function .
* /
2024-04-28 06:21:47 +02:00
export function debounce ( func , timeout = debounce _timeout . standard ) {
2023-07-20 19:32:15 +02:00
let timer ;
2024-07-22 21:33:48 +02:00
let fn = ( ... args ) => {
2023-07-20 19:32:15 +02:00
clearTimeout ( timer ) ;
timer = setTimeout ( ( ) => { func . apply ( this , args ) ; } , timeout ) ;
2024-07-22 21:20:03 +02:00
debounceMap . set ( func , timer ) ;
2024-07-22 21:33:48 +02:00
debounceMap . set ( fn , timer ) ;
2023-07-20 19:32:15 +02:00
} ;
2024-07-22 21:33:48 +02:00
return fn ;
2023-07-20 19:32:15 +02:00
}
2024-07-22 21:20:03 +02:00
/ * *
2024-07-22 21:33:48 +02:00
* Cancels a scheduled debounced function .
* Does nothing if the function is not debounced or not scheduled .
* @ param { function } func The function to cancel . Either the original or the debounced function .
2024-07-22 21:20:03 +02:00
* /
export function cancelDebounce ( func ) {
if ( debounceMap . has ( func ) ) {
clearTimeout ( debounceMap . get ( func ) ) ;
debounceMap . delete ( func ) ;
}
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function throttle ( func , limit = 300 ) {
let lastCall ;
return ( ... args ) => {
const now = Date . now ( ) ;
if ( ! lastCall || ( now - lastCall ) >= limit ) {
lastCall = now ;
func . apply ( this , args ) ;
}
} ;
}
2024-07-07 19:12:04 +02:00
/ * *
* Creates a debounced throttle 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 debouncedThrottle ( func , limit = 300 ) {
let last , deferTimer ;
let db = debounce ( func ) ;
2024-08-05 05:03:46 +02:00
return function ( ) {
2024-07-07 19:12:04 +02:00
let now = + new Date , args = arguments ;
2024-08-05 05:03:46 +02:00
if ( ! last || ( last && now < last + limit ) ) {
2024-07-07 19:12:04 +02:00
clearTimeout ( deferTimer ) ;
db . apply ( this , args ) ;
2024-08-05 05:03:46 +02:00
deferTimer = setTimeout ( function ( ) {
2024-07-07 19:12:04 +02:00
last = now ;
func . apply ( this , args ) ;
} , limit ) ;
} else {
last = now ;
func . apply ( this , args ) ;
}
} ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Checks if an element is in the viewport .
2023-08-22 21:45:12 +02:00
* @ param { Element } el The element to check .
2023-08-22 12:07:24 +02:00
* @ returns { boolean } True if the element is in the viewport , false otherwise .
* /
2023-07-20 19:32:15 +02:00
export function isElementInViewport ( el ) {
2024-06-16 13:56:08 +02:00
if ( ! el ) {
return false ;
}
2023-12-02 19:04:51 +01:00
if ( typeof jQuery === 'function' && el instanceof jQuery ) {
2023-07-20 19:32:15 +02:00
el = el [ 0 ] ;
}
var rect = el . getBoundingClientRect ( ) ;
return (
rect . top >= 0 &&
rect . left >= 0 &&
rect . bottom <= ( window . innerHeight || document . documentElement . clientHeight ) && /* or $(window).height() */
rect . right <= ( window . innerWidth || document . documentElement . clientWidth ) /* or $(window).width() */
) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function getUniqueName ( name , exists ) {
let i = 1 ;
let baseName = name ;
while ( exists ( name ) ) {
name = ` ${ baseName } ( ${ i } ) ` ;
i ++ ;
}
return name ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 ) ) ;
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* 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 ;
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* 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'
* /
2023-07-20 19:32:15 +02:00
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
2023-08-22 12:07:24 +02:00
return str . substring ( 0 , count . index ) + ( Number ( count [ 0 ] ) + 1 ) ;
2023-12-02 16:15:03 +01:00
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* 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!'
* /
2023-07-20 19:32:15 +02:00
export function stringFormat ( format ) {
const args = Array . prototype . slice . call ( arguments , 1 ) ;
return format . replace ( /{(\d+)}/g , function ( match , number ) {
return typeof args [ number ] != 'undefined'
? args [ number ]
2023-12-02 20:56:16 +01:00
: match ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 16:15:03 +01:00
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function saveCaretPosition ( element ) {
// Get the current selection
const selection = window . getSelection ( ) ;
// If the selection is empty, return null
if ( selection . rangeCount === 0 ) {
return null ;
}
// Get the range of the current selection
const range = selection . getRangeAt ( 0 ) ;
// If the range is not within the specified element, return null
if ( ! element . contains ( range . commonAncestorContainer ) ) {
return null ;
}
// Return an object with the start and end offsets of the range
const position = {
start : range . startOffset ,
2023-12-02 21:06:57 +01:00
end : range . endOffset ,
2023-07-20 19:32:15 +02:00
} ;
console . debug ( 'Caret saved' , position ) ;
return position ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function restoreCaretPosition ( element , position ) {
// If the position is null, do nothing
if ( ! position ) {
return ;
}
console . debug ( 'Caret restored' , position ) ;
// Create a new range object
const range = new Range ( ) ;
// Set the start and end positions of the range within the element
range . setStart ( element . childNodes [ 0 ] , position . start ) ;
range . setEnd ( element . childNodes [ 0 ] , position . end ) ;
// Create a new selection object and set the range
const selection = window . getSelection ( ) ;
selection . removeAllRanges ( ) ;
selection . addRange ( range ) ;
}
export async function resetScrollHeight ( element ) {
$ ( element ) . css ( 'height' , '0px' ) ;
$ ( element ) . css ( 'height' , $ ( element ) . prop ( 'scrollHeight' ) + 3 + 'px' ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export async function initScrollHeight ( element ) {
await delay ( 1 ) ;
2023-12-02 19:04:51 +01:00
const curHeight = Number ( $ ( element ) . css ( 'height' ) . replace ( 'px' , '' ) ) ;
const curScrollHeight = Number ( $ ( element ) . prop ( 'scrollHeight' ) ) ;
2023-07-20 19:32:15 +02:00
const diff = curScrollHeight - curHeight ;
2023-12-02 20:11:06 +01:00
if ( diff < 3 ) { return ; } //happens when the div isn't loaded yet
2023-07-20 19:32:15 +02:00
const newHeight = curHeight + diff + 3 ; //the +3 here is to account for padding/line-height on text inputs
//console.log(`init height to ${newHeight}`);
2023-12-02 19:04:51 +01:00
$ ( element ) . css ( 'height' , '' ) ;
$ ( element ) . css ( 'height' , ` ${ newHeight } px ` ) ;
2023-07-20 19:32:15 +02:00
//resetScrollHeight(element);
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function sortByCssOrder ( a , b ) {
const _a = Number ( $ ( a ) . css ( 'order' ) ) ;
const _b = Number ( $ ( b ) . css ( 'order' ) ) ;
return _a - _b ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Trims a string to the end of a nearest sentence .
* @ param { string } input The string to trim .
* @ returns { string } The trimmed string .
* @ example
2023-09-09 16:31:27 +02:00
* trimToEndSentence ( 'Hello, world! I am from' ) ; // 'Hello, world!'
2023-08-22 12:07:24 +02:00
* /
2024-09-22 18:55:43 +02:00
export function trimToEndSentence ( input ) {
2024-06-06 20:45:47 +02:00
if ( ! input ) {
return '' ;
}
2024-06-11 22:37:00 +02:00
const isEmoji = x => / ( \ p { Emoji _Presentation } | \ p { Extended _Pictographic } ) / gu . test ( x ) ;
2024-05-30 01:29:28 +02:00
const punctuation = new Set ( [ '.' , '!' , '?' , '*' , '"' , ')' , '}' , '`' , ']' , '$' , '。' , '! ' , '? ' , '”' , ') ' , '】' , '’ ' , '」' , '_' ] ) ; // extend this as you see fit
2023-07-20 19:32:15 +02:00
let last = - 1 ;
2024-06-11 22:37:00 +02:00
const characters = Array . from ( input ) ;
for ( let i = characters . length - 1 ; i >= 0 ; i -- ) {
const char = characters [ i ] ;
2023-07-20 19:32:15 +02:00
2024-06-11 22:37:00 +02:00
if ( punctuation . has ( char ) || isEmoji ( char ) ) {
if ( i > 0 && /[\s\n]/ . test ( characters [ i - 1 ] ) ) {
2024-02-02 17:42:31 +01:00
last = i - 1 ;
} else {
last = i ;
}
2023-07-20 19:32:15 +02:00
break ;
}
}
if ( last === - 1 ) {
return input . trimEnd ( ) ;
}
2024-06-11 22:37:00 +02:00
return characters . slice ( 0 , last + 1 ) . join ( '' ) . trimEnd ( ) ;
2023-07-20 19:32:15 +02:00
}
2023-09-09 16:31:27 +02:00
export function trimToStartSentence ( input ) {
2024-06-06 20:45:47 +02:00
if ( ! input ) {
return '' ;
}
2023-12-02 19:04:51 +01:00
let p1 = input . indexOf ( '.' ) ;
let p2 = input . indexOf ( '!' ) ;
let p3 = input . indexOf ( '?' ) ;
let p4 = input . indexOf ( '\n' ) ;
2023-09-09 16:31:27 +02:00
let first = p1 ;
let skip1 = false ;
if ( p2 > 0 && p2 < first ) { first = p2 ; }
if ( p3 > 0 && p3 < first ) { first = p3 ; }
if ( p4 > 0 && p4 < first ) { first = p4 ; skip1 = true ; }
if ( first > 0 ) {
if ( skip1 ) {
return input . substring ( first + 1 ) ;
} else {
return input . substring ( first + 2 ) ;
}
}
return input ;
}
2023-11-18 18:23:58 +01:00
/ * *
* Format bytes as human - readable text .
*
* @ param bytes Number of bytes .
* @ param si True to use metric ( SI ) units , aka powers of 1000. False to use
* binary ( IEC ) , aka powers of 1024.
* @ param dp Number of decimal places to display .
*
* @ return Formatted string .
* /
export function humanFileSize ( bytes , si = false , dp = 1 ) {
const thresh = si ? 1000 : 1024 ;
if ( Math . abs ( bytes ) < thresh ) {
return bytes + ' B' ;
}
const units = si
? [ 'kB' , 'MB' , 'GB' , 'TB' , 'PB' , 'EB' , 'ZB' , 'YB' ]
: [ 'KiB' , 'MiB' , 'GiB' , 'TiB' , 'PiB' , 'EiB' , 'ZiB' , 'YiB' ] ;
let u = - 1 ;
const r = 10 * * dp ;
do {
bytes /= thresh ;
++ u ;
} while ( Math . round ( Math . abs ( bytes ) * r ) / r >= thresh && u < units . length - 1 ) ;
return bytes . toFixed ( dp ) + ' ' + units [ u ] ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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
* /
2023-07-20 19:32:15 +02:00
export function countOccurrences ( string , character ) {
let count = 0 ;
for ( let i = 0 ; i < string . length ; i ++ ) {
2023-10-20 22:52:23 +02:00
if ( string . substring ( i , i + character . length ) === character ) {
2023-07-20 19:32:15 +02:00
count ++ ;
}
}
return count ;
}
2023-11-24 14:18:49 +01:00
/ * *
* Checks if a string is "true" value .
* @ param { string } arg String to check
* @ returns { boolean } True if the string is true , false otherwise .
* /
export function isTrueBoolean ( arg ) {
2023-12-01 23:04:38 +01:00
return [ 'on' , 'true' , '1' ] . includes ( arg ? . trim ( ) ? . toLowerCase ( ) ) ;
2023-11-24 14:18:49 +01:00
}
/ * *
* Checks if a string is "false" value .
* @ param { string } arg String to check
* @ returns { boolean } True if the string is false , false otherwise .
* /
export function isFalseBoolean ( arg ) {
2023-12-01 23:04:38 +01:00
return [ 'off' , 'false' , '0' ] . includes ( arg ? . trim ( ) ? . toLowerCase ( ) ) ;
2023-11-24 14:18:49 +01:00
}
2024-06-23 18:43:56 +02:00
/ * *
* Parses an array either as a comma - separated string or as a JSON array .
* @ param { string } value String to parse
* @ returns { string [ ] } The parsed array .
* /
export function parseStringArray ( value ) {
if ( ! value || typeof value !== 'string' ) return [ ] ;
try {
const parsedValue = JSON . parse ( value ) ;
if ( ! Array . isArray ( parsedValue ) ) {
throw new Error ( 'Not an array' ) ;
}
return parsedValue . map ( x => String ( x ) ) ;
} catch ( e ) {
return value . split ( ',' ) . map ( x => x . trim ( ) ) . filter ( x => x ) ;
}
}
2023-08-22 12:07:24 +02:00
/ * *
* 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
* /
2023-07-20 19:32:15 +02:00
export function isOdd ( number ) {
return number % 2 !== 0 ;
}
2024-07-26 18:29:41 +02:00
/ * *
* Compare two moment objects for sorting .
2024-10-17 01:02:45 +02:00
* @ param { import ( 'moment' ) . Moment } a The first moment object .
* @ param { import ( 'moment' ) . Moment } b The second moment object .
2024-07-26 18:29:41 +02:00
* @ 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 ;
} else if ( a . isAfter ( b ) ) {
return - 1 ;
} else {
return 0 ;
}
}
2024-03-12 19:29:07 +01:00
const dateCache = new Map ( ) ;
/ * *
* Cached version of moment ( ) to avoid re - parsing the same date strings .
2024-03-12 19:45:30 +01:00
* Important : Moment objects are mutable , so use clone ( ) before modifying them !
2024-05-11 13:49:11 +02:00
* @ param { string | number } timestamp String or number representing a date .
2024-10-17 01:02:45 +02:00
* @ returns { import ( 'moment' ) . Moment } Moment object
2024-03-12 19:29:07 +01:00
* /
2023-07-20 19:32:15 +02:00
export function timestampToMoment ( timestamp ) {
2024-03-12 19:29:07 +01:00
if ( dateCache . has ( timestamp ) ) {
return dateCache . get ( timestamp ) ;
}
2024-07-26 18:29:41 +02:00
const iso8601 = parseTimestamp ( timestamp ) ;
2024-10-28 10:01:48 +01:00
const objMoment = iso8601 ? moment ( iso8601 ) . locale ( getCurrentLocale ( ) ) : moment . invalid ( ) ;
2024-07-26 18:29:41 +02:00
dateCache . set ( timestamp , objMoment ) ;
return objMoment ;
2024-03-12 19:29:07 +01:00
}
2024-07-25 00:27:39 +02:00
/ * *
* Parses a timestamp and returns a moment object representing the parsed date and time .
* @ param { string | number } timestamp - The timestamp to parse . It can be a string or a number .
2024-07-26 18:29:41 +02:00
* @ returns { string } - If the timestamp is valid , returns an ISO 8601 string .
2024-07-25 00:27:39 +02:00
* /
2024-03-12 19:29:07 +01:00
function parseTimestamp ( timestamp ) {
2024-07-26 18:29:41 +02:00
if ( ! timestamp ) return ;
2023-07-20 19:32:15 +02:00
2023-11-10 20:56:25 +01:00
// Unix time (legacy TAI / tags)
2024-06-09 10:15:17 +02:00
if ( typeof timestamp === 'number' || /^\d+$/ . test ( timestamp ) ) {
2024-07-26 18:29:41 +02:00
const unixTime = Number ( timestamp ) ;
const isValid = Number . isFinite ( unixTime ) && ! Number . isNaN ( unixTime ) && unixTime >= 0 ;
if ( ! isValid ) return ;
return new Date ( unixTime ) . toISOString ( ) ;
2023-07-20 19:32:15 +02:00
}
2024-07-24 23:54:27 +02:00
let dtFmt = [ ] ;
2023-07-20 19:32:15 +02:00
2024-07-24 23:54:27 +02:00
// meridiem-based format
const convertFromMeridiemBased = ( _ , month , day , year , hour , minute , meridiem ) => {
2023-12-02 19:04:51 +01:00
const monthNum = moment ( ) . month ( month ) . format ( 'MM' ) ;
2023-07-20 19:32:15 +02:00
const hour24 = meridiem . toLowerCase ( ) === 'pm' ? ( parseInt ( hour , 10 ) % 12 ) + 12 : parseInt ( hour , 10 ) % 12 ;
2023-12-02 19:04:51 +01:00
return ` ${ year } - ${ monthNum } - ${ day . padStart ( 2 , '0' ) } T ${ hour24 . toString ( ) . padStart ( 2 , '0' ) } : ${ minute . padStart ( 2 , '0' ) } :00 ` ;
2023-07-20 19:32:15 +02:00
} ;
2024-07-24 23:54:27 +02:00
// June 19, 2023 2:20pm
dtFmt . push ( { callback : convertFromMeridiemBased , pattern : /(\w+)\s(\d{1,2}),\s(\d{4})\s(\d{1,2}):(\d{1,2})(am|pm)/i } ) ;
// ST "humanized" format patterns
const convertFromHumanized = ( _ , year , month , day , hour , min , sec , ms ) => {
ms = typeof ms !== 'undefined' ? ` . ${ ms . padStart ( 3 , '0' ) } ` : '' ;
return ` ${ year . padStart ( 4 , '0' ) } - ${ month . padStart ( 2 , '0' ) } - ${ day . padStart ( 2 , '0' ) } T ${ hour . padStart ( 2 , '0' ) } : ${ min . padStart ( 2 , '0' ) } : ${ sec . padStart ( 2 , '0' ) } ${ ms } Z ` ;
} ;
// 2024-7-12@01h31m37s
dtFmt . push ( { callback : convertFromHumanized , pattern : /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s/ } ) ;
// 2024-6-5 @14h 56m 50s 682ms
dtFmt . push ( { callback : convertFromHumanized , pattern : /(\d{4})-(\d{1,2})-(\d{1,2}) @(\d{1,2})h (\d{1,2})m (\d{1,2})s (\d{1,3})ms/ } ) ;
for ( const x of dtFmt ) {
2024-07-25 00:23:02 +02:00
let rgxMatch = timestamp . match ( x . pattern ) ;
2024-07-24 23:54:27 +02:00
if ( ! rgxMatch ) continue ;
2024-07-26 18:29:41 +02:00
return x . callback ( ... rgxMatch ) ;
2023-07-20 19:32:15 +02:00
}
2024-07-26 18:29:41 +02:00
return ;
2023-07-20 19:32:15 +02:00
}
2023-08-22 12:07:24 +02:00
/ * * S p l i t s t r i n g t o p a r t s n o m o r e t h a n l e n g t h i n s i z e .
* @ 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' , ' ' , '' ] ) {
2024-04-17 01:09:22 +02:00
// Invalid length
if ( length <= 0 ) {
return [ input ] ;
}
2023-08-22 12:07:24 +02:00
const delim = delimiters [ 0 ] ? ? '' ;
2023-07-20 19:32:15 +02:00
const parts = input . split ( delim ) ;
const flatParts = parts . flatMap ( p => {
if ( p . length < length ) return p ;
2024-04-20 00:24:46 +02:00
return splitRecursive ( p , length , delimiters . slice ( 1 ) ) ;
2023-07-20 19:32:15 +02:00
} ) ;
// Merge short chunks
const result = [ ] ;
let currentChunk = '' ;
for ( let i = 0 ; i < flatParts . length ; ) {
currentChunk = flatParts [ i ] ;
let j = i + 1 ;
while ( j < flatParts . length ) {
const nextChunk = flatParts [ j ] ;
if ( currentChunk . length + nextChunk . length + delim . length <= length ) {
currentChunk += delim + nextChunk ;
} else {
break ;
}
j ++ ;
}
i = j ;
result . push ( currentChunk ) ;
}
return result ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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
* /
2023-07-20 19:32:15 +02:00
export function isDataURL ( str ) {
2023-12-02 16:17:31 +01:00
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@/?#]+)?$/i ;
2023-07-20 19:32:15 +02:00
return regex . test ( str ) ;
}
2024-05-14 00:08:31 +02:00
/ * *
* Gets the size of an image from a data URL .
* @ param { string } dataUrl Image data URL
* @ returns { Promise < { width : number , height : number } > } Image size
* /
export function getImageSizeFromDataURL ( dataUrl ) {
const image = new Image ( ) ;
image . src = dataUrl ;
return new Promise ( ( resolve , reject ) => {
image . onload = function ( ) {
resolve ( { width : image . width , height : image . height } ) ;
} ;
image . onerror = function ( ) {
reject ( new Error ( 'Failed to load image' ) ) ;
} ;
} ) ;
}
2023-07-20 19:32:15 +02:00
export function getCharaFilename ( chid ) {
const context = getContext ( ) ;
2024-07-01 23:16:46 +02:00
const fileName = context . characters [ chid ? ? context . characterId ] ? . avatar ;
2023-07-20 19:32:15 +02:00
if ( fileName ) {
2023-12-02 20:11:06 +01:00
return fileName . replace ( /\.[^/.]+$/ , '' ) ;
2023-07-20 19:32:15 +02:00
}
}
2023-08-22 12:07:24 +02:00
/ * *
* 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']
* /
2023-07-30 22:10:37 +02:00
export function extractAllWords ( value ) {
const words = [ ] ;
if ( ! value ) {
return words ;
}
const matches = value . matchAll ( /\b\w+\b/gim ) ;
for ( let match of matches ) {
words . push ( match [ 0 ] . toLowerCase ( ) ) ;
}
return words ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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\\$'
* /
2023-07-20 19:32:15 +02:00
export function escapeRegex ( string ) {
return string . replace ( /[/\-\\^$*+?.()|[\]{}]/g , '\\$&' ) ;
}
2024-05-17 00:14:07 +02:00
/ * *
* Instantiates a regular expression from a string .
* @ param { string } input The input string .
* @ returns { RegExp } The regular expression instance .
* @ copyright Originally from : https : //github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
* /
export function regexFromString ( input ) {
try {
// Parse input
var m = input . match ( /(\/?)(.+)\1([a-z]*)/i ) ;
// Invalid flags
if ( m [ 3 ] && ! /^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/ . test ( m [ 3 ] ) ) {
return RegExp ( input ) ;
}
// Create the regular expression
return new RegExp ( m [ 2 ] , m [ 3 ] ) ;
} catch {
return ;
}
}
2023-12-12 21:11:23 +01:00
export class Stopwatch {
/ * *
* Initializes a Stopwatch class .
* @ param { number } interval Update interval in milliseconds . Must be a finite number above zero .
* /
constructor ( interval ) {
if ( isNaN ( interval ) || ! isFinite ( interval ) || interval <= 0 ) {
console . warn ( 'Invalid interval for Stopwatch, setting to 1' ) ;
interval = 1 ;
}
this . interval = interval ;
this . lastAction = Date . now ( ) ;
}
/ * *
* Executes a function if the interval passed .
* @ param { ( arg0 : any ) => any } action Action function
* @ returns Promise < void >
* /
async tick ( action ) {
const passed = ( Date . now ( ) - this . lastAction ) ;
if ( passed < this . interval ) {
return ;
}
await action ( ) ;
this . lastAction = Date . now ( ) ;
}
}
2023-08-22 12:07:24 +02:00
/ * *
* Provides an interface for rate limiting function calls .
* /
2023-07-20 19:32:15 +02:00
export class RateLimiter {
2023-08-22 12:07:24 +02:00
/ * *
* 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 ( ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
_waitRemainingTime ( abortSignal ) {
const currentTime = Date . now ( ) ;
2023-08-22 12:07:24 +02:00
const elapsedTime = currentTime - this . lastResolveTime ;
const remainingTime = Math . max ( 0 , this . interval - elapsedTime ) ;
2023-07-20 19:32:15 +02:00
return new Promise ( ( resolve , reject ) => {
const timeoutId = setTimeout ( ( ) => {
resolve ( ) ;
} , remainingTime ) ;
if ( abortSignal ) {
abortSignal . addEventListener ( 'abort' , ( ) => {
clearTimeout ( timeoutId ) ;
reject ( new Error ( 'Aborted' ) ) ;
} ) ;
}
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
async waitForResolve ( abortSignal ) {
2023-08-22 12:07:24 +02:00
await this . pendingResolve ;
this . pendingResolve = this . _waitRemainingTime ( abortSignal ) ;
2023-07-20 19:32:15 +02:00
// Update the last resolve time
2023-08-22 12:07:24 +02:00
this . lastResolveTime = Date . now ( ) + this . interval ;
console . debug ( ` RateLimiter.waitForResolve() ${ this . lastResolveTime } ` ) ;
2023-07-20 19:32:15 +02:00
}
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export function extractDataFromPng ( data , identifier = 'chara' ) {
2023-12-02 19:04:51 +01:00
console . log ( 'Attempting PNG import...' ) ;
2023-07-20 19:32:15 +02:00
let uint8 = new Uint8Array ( 4 ) ;
let uint32 = new Uint32Array ( uint8 . buffer ) ;
//check if png header is valid
if ( ! data || data [ 0 ] !== 0x89 || data [ 1 ] !== 0x50 || data [ 2 ] !== 0x4E || data [ 3 ] !== 0x47 || data [ 4 ] !== 0x0D || data [ 5 ] !== 0x0A || data [ 6 ] !== 0x1A || data [ 7 ] !== 0x0A ) {
2023-12-02 20:11:06 +01:00
console . log ( 'PNG header invalid' ) ;
2023-07-20 19:32:15 +02:00
return null ;
}
let ended = false ;
let chunks = [ ] ;
let idx = 8 ;
while ( idx < data . length ) {
// Read the length of the current chunk,
// which is stored as a Uint32.
uint8 [ 3 ] = data [ idx ++ ] ;
uint8 [ 2 ] = data [ idx ++ ] ;
uint8 [ 1 ] = data [ idx ++ ] ;
uint8 [ 0 ] = data [ idx ++ ] ;
// Chunk includes name/type for CRC check (see below).
let length = uint32 [ 0 ] + 4 ;
let chunk = new Uint8Array ( length ) ;
chunk [ 0 ] = data [ idx ++ ] ;
chunk [ 1 ] = data [ idx ++ ] ;
chunk [ 2 ] = data [ idx ++ ] ;
chunk [ 3 ] = data [ idx ++ ] ;
// Get the name in ASCII for identification.
let name = (
String . fromCharCode ( chunk [ 0 ] ) +
String . fromCharCode ( chunk [ 1 ] ) +
String . fromCharCode ( chunk [ 2 ] ) +
String . fromCharCode ( chunk [ 3 ] )
) ;
// The IHDR header MUST come first.
if ( ! chunks . length && name !== 'IHDR' ) {
console . log ( 'Warning: IHDR header missing' ) ;
}
// The IEND header marks the end of the file,
// so on discovering it break out of the loop.
if ( name === 'IEND' ) {
ended = true ;
chunks . push ( {
name : name ,
2023-12-02 21:06:57 +01:00
data : new Uint8Array ( 0 ) ,
2023-07-20 19:32:15 +02:00
} ) ;
break ;
}
// Read the contents of the chunk out of the main buffer.
for ( let i = 4 ; i < length ; i ++ ) {
chunk [ i ] = data [ idx ++ ] ;
}
// Read out the CRC value for comparison.
// It's stored as an Int32.
uint8 [ 3 ] = data [ idx ++ ] ;
uint8 [ 2 ] = data [ idx ++ ] ;
uint8 [ 1 ] = data [ idx ++ ] ;
uint8 [ 0 ] = data [ idx ++ ] ;
// The chunk data is now copied to remove the 4 preceding
// bytes used for the chunk name/type.
let chunkData = new Uint8Array ( chunk . buffer . slice ( 4 ) ) ;
chunks . push ( {
name : name ,
2023-12-02 21:06:57 +01:00
data : chunkData ,
2023-07-20 19:32:15 +02:00
} ) ;
}
if ( ! ended ) {
console . log ( '.png file ended prematurely: no IEND header was found' ) ;
}
//find the chunk with the chara name, just check first and last letter
let found = chunks . filter ( x => (
2023-12-02 19:04:51 +01:00
x . name == 'tEXt'
2023-07-20 19:32:15 +02:00
&& x . data . length > identifier . length
&& x . data . slice ( 0 , identifier . length ) . every ( ( v , i ) => String . fromCharCode ( v ) == identifier [ i ] ) ) ) ;
if ( found . length == 0 ) {
console . log ( 'PNG Image contains no data' ) ;
return null ;
} else {
try {
2023-12-02 19:04:51 +01:00
let b64buf = '' ;
2023-07-20 19:32:15 +02:00
let bytes = found [ 0 ] . data ; //skip the chara
for ( let i = identifier . length + 1 ; i < bytes . length ; i ++ ) {
b64buf += String . fromCharCode ( bytes [ i ] ) ;
}
let decoded = JSON . parse ( atob ( b64buf ) ) ;
console . log ( decoded ) ;
return decoded ;
} catch ( e ) {
2023-12-02 19:04:51 +01:00
console . log ( 'Error decoding b64 in image: ' + e ) ;
2023-07-20 19:32:15 +02:00
return null ;
}
}
}
2024-05-22 21:11:39 +02:00
/ * *
* Sends a request to the server to sanitize a given filename
*
* @ param { string } fileName - The name of the file to sanitize
* @ returns { Promise < string > } A Promise that resolves to the sanitized filename if successful , or rejects with an error message if unsuccessful
* /
export async function getSanitizedFilename ( fileName ) {
try {
const result = await fetch ( '/api/files/sanitize-filename' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( {
fileName : fileName ,
} ) ,
} ) ;
if ( ! result . ok ) {
const error = await result . text ( ) ;
throw new Error ( error ) ;
}
const responseData = await result . json ( ) ;
return responseData . fileName ;
} catch ( error ) {
toastr . error ( String ( error ) , 'Could not sanitize fileName' ) ;
console . error ( 'Could not sanitize fileName' , error ) ;
throw error ;
}
}
2023-08-20 06:15:57 +02:00
/ * *
* Sends a base64 encoded image to the backend to be saved as a file .
2023-08-20 11:37:38 +02:00
*
2023-08-20 06:15:57 +02:00
* @ param { string } base64Data - The base64 encoded image data .
* @ param { string } characterName - The character name to determine the sub - directory for saving .
* @ param { string } ext - The file extension for the image ( e . g . , 'jpg' , 'png' , 'webp' ) .
2023-08-20 11:37:38 +02:00
*
* @ returns { Promise < string > } - Resolves to the saved image ' s path on the server .
2023-08-20 06:15:57 +02:00
* Rejects with an error if the upload fails .
* /
2023-12-02 19:04:51 +01:00
export async function saveBase64AsFile ( base64Data , characterName , filename = '' , ext ) {
2023-08-20 05:53:34 +02:00
// Construct the full data URL
const format = ext ; // Extract the file extension (jpg, png, webp)
const dataURL = ` data:image/ ${ format } ;base64, ${ base64Data } ` ;
// Prepare the request body
const requestBody = {
image : dataURL ,
2023-08-20 07:41:58 +02:00
ch _name : characterName ,
2024-03-03 19:39:20 +01:00
filename : String ( filename ) . replace ( /\./g , '_' ) ,
2023-08-20 05:53:34 +02:00
} ;
// Send the data URL to your backend using fetch
2024-03-19 23:59:06 +01:00
const response = await fetch ( '/api/images/upload' , {
2023-08-20 05:53:34 +02:00
method : 'POST' ,
body : JSON . stringify ( requestBody ) ,
headers : {
... getRequestHeaders ( ) ,
2023-12-02 21:06:57 +01:00
'Content-Type' : 'application/json' ,
2023-08-20 05:53:34 +02:00
} ,
} ) ;
// If the response is successful, get the saved image path from the server's response
if ( response . ok ) {
const responseData = await response . json ( ) ;
2023-12-05 23:55:52 +01:00
return responseData . path ;
2023-08-20 05:53:34 +02:00
} else {
const errorData = await response . json ( ) ;
throw new Error ( errorData . error || 'Failed to upload the image to the server' ) ;
}
}
2023-08-21 17:21:32 +02:00
/ * *
* Loads either a CSS or JS file and appends it to the appropriate document section .
2023-09-08 21:44:06 +02:00
*
2023-08-21 17:21:32 +02:00
* @ param { string } url - The URL of the file to be loaded .
* @ param { string } type - The type of file to load : "css" or "js" .
* @ returns { Promise } - Resolves when the file has loaded , rejects if there ' s an error or invalid type .
* /
export function loadFileToDocument ( url , type ) {
return new Promise ( ( resolve , reject ) => {
let element ;
2023-12-02 19:04:51 +01:00
if ( type === 'css' ) {
element = document . createElement ( 'link' ) ;
element . rel = 'stylesheet' ;
2023-08-21 17:21:32 +02:00
element . href = url ;
2023-12-02 19:04:51 +01:00
} else if ( type === 'js' ) {
element = document . createElement ( 'script' ) ;
2023-08-21 17:21:32 +02:00
element . src = url ;
} else {
2023-12-02 19:04:51 +01:00
reject ( 'Invalid type specified' ) ;
2023-08-21 17:21:32 +02:00
return ;
}
element . onload = resolve ;
element . onerror = reject ;
2023-12-02 19:04:51 +01:00
type === 'css'
2023-08-21 17:21:32 +02:00
? document . head . appendChild ( element )
: document . body . appendChild ( element ) ;
} ) ;
}
2024-03-21 13:47:22 +01:00
/ * *
* Ensure that we can import war crime image formats like WEBP and AVIF .
* @ param { File } file Input file
* @ returns { Promise < File > } A promise that resolves to the supported file .
* /
export async function ensureImageFormatSupported ( file ) {
const supportedTypes = [
'image/jpeg' ,
'image/png' ,
'image/bmp' ,
'image/tiff' ,
'image/gif' ,
'image/apng' ,
] ;
if ( supportedTypes . includes ( file . type ) || ! file . type . startsWith ( 'image/' ) ) {
return file ;
}
return await convertImageFile ( file , 'image/png' ) ;
}
/ * *
* Converts an image file to a given format .
* @ param { File } inputFile File to convert
* @ param { string } type Target file type
* @ returns { Promise < File > } A promise that resolves to the converted file .
* /
export async function convertImageFile ( inputFile , type = 'image/png' ) {
const base64 = await getBase64Async ( inputFile ) ;
const thumbnail = await createThumbnail ( base64 , null , null , type ) ;
const blob = await fetch ( thumbnail ) . then ( res => res . blob ( ) ) ;
const outputFile = new File ( [ blob ] , inputFile . name , { type } ) ;
return outputFile ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Creates a thumbnail from a data URL .
* @ param { string } dataUrl The data URL encoded data of the image .
2024-03-21 13:47:22 +01:00
* @ param { number | null } maxWidth The maximum width of the thumbnail .
* @ param { number | null } maxHeight The maximum height of the thumbnail .
2023-11-23 19:50:08 +01:00
* @ param { string } [ type = 'image/jpeg' ] The type of the thumbnail .
2023-08-22 12:07:24 +02:00
* @ returns { Promise < string > } A promise that resolves to the thumbnail data URL .
* /
2024-03-21 13:47:22 +01:00
export function createThumbnail ( dataUrl , maxWidth = null , maxHeight = null , type = 'image/jpeg' ) {
2023-12-14 21:28:22 +01:00
// Someone might pass in a base64 encoded string without the data URL prefix
if ( ! dataUrl . includes ( 'data:' ) ) {
dataUrl = ` data:image/jpeg;base64, ${ dataUrl } ` ;
}
2023-07-20 19:32:15 +02:00
return new Promise ( ( resolve , reject ) => {
const img = new Image ( ) ;
img . src = dataUrl ;
img . onload = ( ) => {
const canvas = document . createElement ( 'canvas' ) ;
const ctx = canvas . getContext ( '2d' ) ;
// Calculate the thumbnail dimensions while maintaining the aspect ratio
const aspectRatio = img . width / img . height ;
let thumbnailWidth = maxWidth ;
let thumbnailHeight = maxHeight ;
2024-03-21 13:47:22 +01:00
if ( maxWidth === null ) {
thumbnailWidth = img . width ;
maxWidth = img . width ;
}
if ( maxHeight === null ) {
thumbnailHeight = img . height ;
maxHeight = img . height ;
}
2023-07-20 19:32:15 +02:00
if ( img . width > img . height ) {
thumbnailHeight = maxWidth / aspectRatio ;
} else {
thumbnailWidth = maxHeight * aspectRatio ;
}
// Set the canvas dimensions and draw the resized image
canvas . width = thumbnailWidth ;
canvas . height = thumbnailHeight ;
ctx . drawImage ( img , 0 , 0 , thumbnailWidth , thumbnailHeight ) ;
// Convert the canvas to a data URL and resolve the promise
2023-11-23 19:50:08 +01:00
const thumbnailDataUrl = canvas . toDataURL ( type ) ;
2023-07-20 19:32:15 +02:00
resolve ( thumbnailDataUrl ) ;
} ;
img . onerror = ( ) => {
reject ( new Error ( 'Failed to load the image.' ) ) ;
} ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* 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 .
* /
2023-07-20 19:32:15 +02:00
export async function waitUntilCondition ( condition , timeout = 1000 , interval = 100 ) {
return new Promise ( ( resolve , reject ) => {
const timeoutId = setTimeout ( ( ) => {
clearInterval ( intervalId ) ;
reject ( new Error ( 'Timed out waiting for condition to be true' ) ) ;
} , timeout ) ;
const intervalId = setInterval ( ( ) => {
if ( condition ( ) ) {
clearTimeout ( timeoutId ) ;
clearInterval ( intervalId ) ;
resolve ( ) ;
}
} , interval ) ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Returns a UUID v4 string .
* @ returns { string } A UUID v4 string .
* @ example
* uuidv4 ( ) ; // '3e2fd9e1-0a7a-4f6d-9aaf-8a7a4babe7eb'
* /
2023-07-20 19:32:15 +02:00
export function uuidv4 ( ) {
2024-06-10 13:20:52 +02:00
if ( 'randomUUID' in crypto ) {
return crypto . randomUUID ( ) ;
}
2023-07-20 19:32:15 +02:00
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( /[xy]/g , function ( c ) {
const r = Math . random ( ) * 16 | 0 ;
const v = c === 'x' ? r : ( r & 0x3 | 0x8 ) ;
return v . toString ( 16 ) ;
} ) ;
}
2023-11-29 16:51:30 +01:00
2024-09-07 20:19:33 +02:00
/ * *
* Collapses multiple spaces in a strings into one .
* @ param { string } s String to process
* @ returns { string } String with collapsed spaces
* /
export function collapseSpaces ( s ) {
return s . replace ( /\s+/g , ' ' ) . trim ( ) ;
}
2024-02-29 15:37:52 +01:00
function postProcessText ( text , collapse = true ) {
2024-04-21 01:05:59 +02:00
// Remove carriage returns
text = text . replace ( /\r/g , '' ) ;
// Replace tabs with spaces
text = text . replace ( /\t/g , ' ' ) ;
// Normalize unicode spaces
text = text . replace ( /\u00A0/g , ' ' ) ;
2023-11-29 16:51:30 +01:00
// Collapse multiple newlines into one
2024-02-29 15:37:52 +01:00
if ( collapse ) {
text = collapseNewlines ( text ) ;
// Trim leading and trailing whitespace, and remove empty lines
text = text . split ( '\n' ) . map ( l => l . trim ( ) ) . filter ( Boolean ) . join ( '\n' ) ;
2024-04-21 01:05:59 +02:00
} else {
// Replace more than 4 newlines with 4 newlines
text = text . replace ( /\n{4,}/g , '\n\n\n\n' ) ;
// Trim lines that contain nothing but whitespace
text = text . split ( '\n' ) . map ( l => / ^ \ s + $ / . test ( l ) ? '' : l ) . join ( '\n' ) ;
2024-02-29 15:37:52 +01:00
}
2023-11-29 16:51:30 +01:00
// Collapse multiple spaces into one (except for newlines)
text = text . replace ( / {2,}/g , ' ' ) ;
// Remove leading and trailing spaces
text = text . trim ( ) ;
return text ;
}
2024-02-29 15:37:52 +01:00
/ * *
* Uses Readability . js to parse the text from a web page .
* @ param { Document } document HTML document
* @ param { string } [ textSelector = 'body' ] The fallback selector for the text to parse .
* @ returns { Promise < string > } A promise that resolves to the parsed text .
* /
export async function getReadableText ( document , textSelector = 'body' ) {
if ( isProbablyReaderable ( document ) ) {
const parser = new Readability ( document ) ;
const article = parser . parse ( ) ;
return postProcessText ( article . textContent , false ) ;
}
const elements = document . querySelectorAll ( textSelector ) ;
const rawText = Array . from ( elements ) . map ( e => e . textContent ) . join ( '\n' ) ;
const text = postProcessText ( rawText ) ;
return text ;
}
2023-11-29 16:51:30 +01:00
/ * *
* Use pdf . js to load and parse text from PDF pages
* @ param { Blob } blob PDF file blob
* @ returns { Promise < string > } A promise that resolves to the parsed text .
* /
export async function extractTextFromPDF ( blob ) {
2024-10-17 19:53:18 +02:00
if ( ! ( 'pdfjsLib' in window ) ) {
await import ( '../lib/pdf.min.mjs' ) ;
await import ( '../lib/pdf.worker.min.mjs' ) ;
}
2023-11-29 16:51:30 +01:00
const buffer = await getFileBuffer ( blob ) ;
const pdf = await pdfjsLib . getDocument ( buffer ) . promise ;
const pages = [ ] ;
for ( let i = 1 ; i <= pdf . numPages ; i ++ ) {
const page = await pdf . getPage ( i ) ;
const textContent = await page . getTextContent ( ) ;
const text = textContent . items . map ( item => item . str ) . join ( ' ' ) ;
pages . push ( text ) ;
}
return postProcessText ( pages . join ( '\n' ) ) ;
}
/ * *
* Use DOMParser to load and parse text from HTML
* @ param { Blob } blob HTML content blob
* @ returns { Promise < string > } A promise that resolves to the parsed text .
* /
2023-12-12 00:08:47 +01:00
export async function extractTextFromHTML ( blob , textSelector = 'body' ) {
2023-11-29 16:51:30 +01:00
const html = await blob . text ( ) ;
const domParser = new DOMParser ( ) ;
const document = domParser . parseFromString ( DOMPurify . sanitize ( html ) , 'text/html' ) ;
2024-02-29 15:37:52 +01:00
return await getReadableText ( document , textSelector ) ;
2023-11-29 16:51:30 +01:00
}
/ * *
* Use showdown to load and parse text from Markdown
* @ param { Blob } blob Markdown content blob
* @ returns { Promise < string > } A promise that resolves to the parsed text .
* /
export async function extractTextFromMarkdown ( blob ) {
const markdown = await blob . text ( ) ;
2024-05-19 23:48:23 +02:00
const text = postProcessText ( markdown , false ) ;
2023-11-29 16:51:30 +01:00
return text ;
}
2024-03-12 00:49:05 +01:00
2024-04-20 00:24:46 +02:00
export async function extractTextFromEpub ( blob ) {
2024-10-17 19:59:42 +02:00
if ( ! ( 'ePub' in window ) ) {
await import ( '../lib/jszip.min.js' ) ;
await import ( '../lib/epub.min.js' ) ;
}
2024-04-20 00:24:46 +02:00
const book = ePub ( blob ) ;
await book . ready ;
const sectionPromises = [ ] ;
book . spine . each ( ( section ) => {
const sectionPromise = ( async ( ) => {
const chapter = await book . load ( section . href ) ;
if ( ! ( chapter instanceof Document ) || ! chapter . body ? . textContent ) {
return '' ;
}
return chapter . body . textContent . trim ( ) ;
} ) ( ) ;
sectionPromises . push ( sectionPromise ) ;
} ) ;
const content = await Promise . all ( sectionPromises ) ;
const text = content . filter ( text => text ) ;
return postProcessText ( text . join ( '\n' ) , false ) ;
}
2024-04-21 15:27:44 +02:00
/ * *
* Extracts text from an Office document using the server plugin .
* @ param { File } blob File to extract text from
* @ returns { Promise < string > } A promise that resolves to the extracted text .
* /
export async function extractTextFromOffice ( blob ) {
async function checkPluginAvailability ( ) {
try {
const result = await fetch ( '/api/plugins/office/probe' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
} ) ;
return result . ok ;
} catch ( error ) {
return false ;
}
}
const isPluginAvailable = await checkPluginAvailability ( ) ;
if ( ! isPluginAvailable ) {
throw new Error ( 'Importing Office documents requires a server plugin. Please refer to the documentation for more information.' ) ;
}
const base64 = await getBase64Async ( blob ) ;
const response = await fetch ( '/api/plugins/office/parse' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { data : base64 } ) ,
} ) ;
if ( ! response . ok ) {
throw new Error ( 'Failed to parse the Office document' ) ;
}
const data = await response . text ( ) ;
return postProcessText ( data , false ) ;
}
2024-03-12 00:49:05 +01:00
/ * *
* Sets a value in an object by a path .
* @ param { object } obj Object to set value in
* @ param { string } path Key path
* @ param { any } value Value to set
* @ returns { void }
* /
export function setValueByPath ( obj , path , value ) {
const keyParts = path . split ( '.' ) ;
let currentObject = obj ;
for ( let i = 0 ; i < keyParts . length - 1 ; i ++ ) {
const part = keyParts [ i ] ;
if ( ! Object . hasOwn ( currentObject , part ) ) {
currentObject [ part ] = { } ;
}
currentObject = currentObject [ part ] ;
}
currentObject [ keyParts [ keyParts . length - 1 ] ] = value ;
}
2024-04-27 10:26:01 +02:00
/ * *
* Flashes the given HTML element via CSS flash animation for a defined period
* @ param { JQuery < HTMLElement > } element - The element to flash
2024-07-10 19:43:58 +02:00
* @ param { number } timespan - A number in milliseconds how the flash should last ( default is 2000 ms . Multiples of 1000 ms work best , as they end with the flash animation being at 100 % opacity )
2024-04-27 10:26:01 +02:00
* /
export function flashHighlight ( element , timespan = 2000 ) {
2024-07-10 19:43:58 +02:00
const flashDuration = 2000 ; // Duration of a single flash cycle in milliseconds
2024-04-27 10:26:01 +02:00
element . addClass ( 'flash animated' ) ;
2024-07-10 19:43:58 +02:00
element . css ( '--animation-duration' , ` ${ flashDuration } ms ` ) ;
// Repeat the flash animation
const intervalId = setInterval ( ( ) => {
element . removeClass ( 'flash animated' ) ;
void element [ 0 ] . offsetWidth ; // Trigger reflow to restart animation
element . addClass ( 'flash animated' ) ;
} , flashDuration ) ;
setTimeout ( ( ) => {
clearInterval ( intervalId ) ;
element . removeClass ( 'flash animated' ) ;
element . css ( '--animation-duration' , '' ) ;
} , timespan ) ;
2024-04-27 10:26:01 +02:00
}
2024-04-30 06:03:41 +02:00
2024-07-10 19:43:58 +02:00
2024-05-27 03:35:03 +02:00
/ * *
* Checks if the given control has an animation applied to it
*
* @ param { HTMLElement } control - The control element to check for animation
* @ returns { boolean } Whether the control has an animation applied
* /
export function hasAnimation ( control ) {
2024-06-23 18:43:56 +02:00
const animatioName = getComputedStyle ( control , null ) [ 'animation-name' ] ;
return animatioName != 'none' ;
2024-05-27 03:35:03 +02:00
}
/ * *
* Run an action once an animation on a control ends . If the control has no animation , the action will be executed immediately .
*
* @ param { HTMLElement } control - The control element to listen for animation end event
* @ param { ( control : * ? ) => void } callback - The callback function to be executed when the animation ends
* /
export function runAfterAnimation ( control , callback ) {
if ( hasAnimation ( control ) ) {
const onAnimationEnd = ( ) => {
control . removeEventListener ( 'animationend' , onAnimationEnd ) ;
callback ( control ) ;
} ;
control . addEventListener ( 'animationend' , onAnimationEnd ) ;
} else {
callback ( control ) ;
}
}
2024-05-22 18:19:01 +02:00
/ * *
* A common base function for case - insensitive and accent - insensitive string comparisons .
*
* @ param { string } a - The first string to compare .
* @ param { string } b - The second string to compare .
* @ param { ( a : string , b : string ) => boolean } comparisonFunction - The function to use for the comparison .
* @ returns { * } - The result of the comparison .
* /
export function compareIgnoreCaseAndAccents ( a , b , comparisonFunction ) {
if ( ! a || ! b ) return comparisonFunction ( a , b ) ; // Return the comparison result if either string is empty
// Normalize and remove diacritics, then convert to lower case
const normalizedA = a . normalize ( 'NFD' ) . replace ( /[\u0300-\u036f]/g , '' ) . toLowerCase ( ) ;
const normalizedB = b . normalize ( 'NFD' ) . replace ( /[\u0300-\u036f]/g , '' ) . toLowerCase ( ) ;
// Check if the normalized strings are equal
return comparisonFunction ( normalizedA , normalizedB ) ;
}
2024-04-30 06:03:41 +02:00
/ * *
* Performs a case - insensitive and accent - insensitive substring search .
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents .
*
2024-05-22 18:19:01 +02:00
* @ param { string } text - The text in which to search for the substring
* @ param { string } searchTerm - The substring to search for in the text
* @ returns { boolean } true if the searchTerm is found within the text , otherwise returns false
2024-04-30 06:03:41 +02:00
* /
export function includesIgnoreCaseAndAccents ( text , searchTerm ) {
2024-05-22 18:19:01 +02:00
return compareIgnoreCaseAndAccents ( text , searchTerm , ( a , b ) => a ? . includes ( b ) === true ) ;
}
2024-04-30 06:03:41 +02:00
2024-05-22 18:19:01 +02:00
/ * *
* Performs a case - insensitive and accent - insensitive equality check .
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents .
*
* @ param { string } a - The first string to compare
* @ param { string } b - The second string to compare
* @ returns { boolean } true if the strings are equal , otherwise returns false
* /
export function equalsIgnoreCaseAndAccents ( a , b ) {
return compareIgnoreCaseAndAccents ( a , b , ( a , b ) => a === b ) ;
2024-04-30 06:03:41 +02:00
}
2024-05-07 02:01:54 +02:00
2024-05-08 20:34:53 +02:00
/ * *
* @ typedef { object } Select2Option The option object for select2 controls
* @ property { string } id - The unique ID inside this select
* @ property { string } text - The text for this option
* @ property { number ? } [ count ] - Optionally show the count how often that option was chosen already
* /
2024-05-07 05:44:18 +02:00
/ * *
* Returns a unique hash as ID for a select2 option text
2024-05-09 23:30:18 +02:00
*
2024-05-07 05:44:18 +02:00
* @ param { string } option - The option
* @ returns { string } A hashed version of that option
* /
export function getSelect2OptionId ( option ) {
return String ( getStringHash ( option ) ) ;
}
2024-05-07 02:01:54 +02:00
/ * *
* Modifies the select2 options by adding not existing one and optionally selecting them
*
* @ param { JQuery < HTMLElement > } element - The "select" element to add the options to
2024-05-08 20:34:53 +02:00
* @ param { string [ ] | Select2Option [ ] } items - The option items to build , add or select
2024-05-07 02:01:54 +02:00
* @ param { object } [ options ] - Optional arguments
* @ param { boolean } [ options . select = false ] - Whether the options should be selected right away
* @ param { object } [ options . changeEventArgs = null ] - Optional event args being passed into the "change" event when its triggered because a new options is selected
* /
export function select2ModifyOptions ( element , items , { select = false , changeEventArgs = null } = { } ) {
if ( ! items . length ) return ;
2024-05-08 20:34:53 +02:00
/** @type {Select2Option[]} */
2024-05-07 05:44:18 +02:00
const dataItems = items . map ( x => typeof x === 'string' ? { id : getSelect2OptionId ( x ) , text : x } : x ) ;
2024-05-07 02:01:54 +02:00
2024-08-05 01:06:37 +02:00
const optionsToSelect = [ ] ;
const newOptions = [ ] ;
2024-05-07 02:01:54 +02:00
dataItems . forEach ( item => {
// Set the value, creating a new option if necessary
2024-05-17 00:14:07 +02:00
if ( element . find ( 'option[value=\'' + item . id + '\']' ) . length ) {
2024-08-05 01:06:37 +02:00
if ( select ) optionsToSelect . push ( item . id ) ;
2024-05-07 02:01:54 +02:00
} else {
// Create a DOM Option and optionally pre-select by default
var newOption = new Option ( item . text , item . id , select , select ) ;
// Append it to the select
2024-08-05 01:06:37 +02:00
newOptions . push ( newOption ) ;
if ( select ) optionsToSelect . push ( item . id ) ;
2024-05-07 02:01:54 +02:00
}
} ) ;
2024-08-05 01:06:37 +02:00
element . append ( newOptions ) ;
if ( optionsToSelect . length ) element . val ( optionsToSelect ) . trigger ( 'change' , changeEventArgs ) ;
2024-05-07 02:01:54 +02:00
}
2024-05-08 20:34:53 +02:00
/ * *
* Returns the ajax settings that can be used on the select2 ajax property to dynamically get the data .
* Can be used on a single global array , querying data from the server or anything similar .
2024-05-09 23:30:18 +02:00
*
2024-05-08 20:34:53 +02:00
* @ param { function ( ) : Select2Option [ ] } dataProvider - The provider / function to retrieve the data - can be as simple as "() => myData" for arrays
2024-05-14 04:51:22 +02:00
* @ return { { transport : ( params , success , failure ) => any } } The ajax object with the transport function to use on the select2 ajax property
2024-05-08 20:34:53 +02:00
* /
2024-05-09 03:31:41 +02:00
export function dynamicSelect2DataViaAjax ( dataProvider ) {
2024-05-08 20:34:53 +02:00
function dynamicSelect2DataTransport ( params , success , failure ) {
var items = dataProvider ( ) ;
// fitering if params.data.q available
if ( params . data && params . data . q ) {
items = items . filter ( function ( item ) {
2024-05-15 00:38:48 +02:00
return includesIgnoreCaseAndAccents ( item . text , params . data . q ) ;
2024-05-08 20:34:53 +02:00
} ) ;
}
var promise = new Promise ( function ( resolve , reject ) {
resolve ( { results : items } ) ;
} ) ;
promise . then ( success ) ;
promise . catch ( failure ) ;
2024-05-17 00:14:07 +02:00
}
2024-05-08 20:34:53 +02:00
const ajax = {
2024-05-17 00:14:07 +02:00
transport : dynamicSelect2DataTransport ,
2024-05-08 20:34:53 +02:00
} ;
return ajax ;
}
2024-05-09 03:31:41 +02:00
2024-05-14 04:51:22 +02:00
/ * *
* Checks whether a given control is a select2 choice element - meaning one of the results being displayed in the select multi select box
* @ param { JQuery < HTMLElement > | HTMLElement } element - The element to check
* @ returns { boolean } Whether this is a choice element
* /
export function isSelect2ChoiceElement ( element ) {
const $element = $ ( element ) ;
return ( $element . hasClass ( 'select2-selection__choice__display' ) || $element . parents ( '.select2-selection__choice__display' ) . length > 0 ) ;
}
2024-05-09 23:30:18 +02:00
/ * *
* Subscribes a 'click' event handler to the choice elements of a select2 multi - select control
*
* @ param { JQuery < HTMLElement > } control The original control the select2 was applied to
2024-05-14 04:51:22 +02:00
* @ param { function ( HTMLElement ) : void } action - The action to execute when a choice element is clicked
2024-05-09 23:30:18 +02:00
* @ param { object } options - Optional parameters
2024-05-10 00:42:35 +02:00
* @ param { boolean } [ options . buttonStyle = false ] - Whether the choices should be styles as a clickable button with color and hover transition , instead of just changed cursor
2024-05-09 23:30:18 +02:00
* @ param { boolean } [ options . closeDrawer = false ] - Whether the drawer should be closed and focus removed after the choice item was clicked
2024-05-10 00:42:35 +02:00
* @ param { boolean } [ options . openDrawer = false ] - Whether the drawer should be opened , even if this click would normally close it
2024-05-09 23:30:18 +02:00
* /
2024-05-10 00:42:35 +02:00
export function select2ChoiceClickSubscribe ( control , action , { buttonStyle = false , closeDrawer = false , openDrawer = false } = { } ) {
2024-05-09 23:30:18 +02:00
// Add class for styling (hover color, changed cursor, etc)
control . addClass ( 'select2_choice_clickable' ) ;
2024-05-10 00:42:35 +02:00
if ( buttonStyle ) control . addClass ( 'select2_choice_clickable_buttonstyle' ) ;
2024-05-09 23:30:18 +02:00
// Get the real container below and create a click handler on that one
const select2Container = control . next ( 'span.select2-container' ) ;
select2Container . on ( 'click' , function ( event ) {
2024-05-14 04:51:22 +02:00
const isChoice = isSelect2ChoiceElement ( event . target ) ;
if ( isChoice ) {
2024-05-09 23:30:18 +02:00
event . preventDefault ( ) ;
// select2 still bubbles the event to open the dropdown. So we close it here and remove focus if we want that
if ( closeDrawer ) {
control . select2 ( 'close' ) ;
setTimeout ( ( ) => select2Container . find ( 'textarea' ) . trigger ( 'blur' ) , debounce _timeout . quick ) ;
}
2024-05-10 00:42:35 +02:00
if ( openDrawer ) {
control . select2 ( 'open' ) ;
}
2024-05-09 23:30:18 +02:00
// Now execute the actual action that was subscribed
action ( event . target ) ;
}
} ) ;
}
2024-05-09 03:31:41 +02:00
/ * *
* Applies syntax highlighting to a given regex string by generating HTML with classes
2024-05-09 23:30:18 +02:00
*
2024-05-09 03:31:41 +02:00
* @ param { string } regexStr - The javascript compatible regex string
* @ returns { string } The html representation of the highlighted regex
* /
export function highlightRegex ( regexStr ) {
2024-09-18 18:20:31 +02:00
// Function to escape special characters for safety or readability
const escape = ( str ) => str . replace ( /[&<>"'\x01]/g , match => ( {
'&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , '\'' : ''' , '\x01' : '\\x01' ,
2024-05-09 03:31:41 +02:00
} ) [ match ] ) ;
2024-09-18 18:20:31 +02:00
// Replace special characters with their escaped forms
regexStr = escape ( regexStr ) ;
2024-05-09 03:31:41 +02:00
// Patterns that we want to highlight only if they are not escaped
2024-05-19 21:16:14 +02:00
function getPatterns ( ) {
try {
return {
brackets : new RegExp ( '(?<!\\\\)\\[.*?\\]' , 'g' ) , // Non-escaped square brackets
quantifiers : new RegExp ( '(?<!\\\\)[*+?{}]' , 'g' ) , // Non-escaped quantifiers
operators : new RegExp ( '(?<!\\\\)[|.^$()]' , 'g' ) , // Non-escaped operators like | and ()
specialChars : new RegExp ( '\\\\.' , 'g' ) ,
flags : new RegExp ( '(?<=\\/)([gimsuy]*)$' , 'g' ) , // Match trailing flags
delimiters : new RegExp ( '^\\/|(?<![\\\\<])\\/' , 'g' ) , // Match leading or trailing delimiters
} ;
} catch ( error ) {
return {
brackets : new RegExp ( '(\\\\)?\\[.*?\\]' , 'g' ) , // Non-escaped square brackets
quantifiers : new RegExp ( '(\\\\)?[*+?{}]' , 'g' ) , // Non-escaped quantifiers
operators : new RegExp ( '(\\\\)?[|.^$()]' , 'g' ) , // Non-escaped operators like | and ()
specialChars : new RegExp ( '\\\\.' , 'g' ) ,
flags : new RegExp ( '/([gimsuy]*)$' , 'g' ) , // Match trailing flags
delimiters : new RegExp ( '^/|[^\\\\](/)' , 'g' ) , // Match leading or trailing delimiters
} ;
}
}
const patterns = getPatterns ( ) ;
2024-05-09 03:31:41 +02:00
// Function to replace each pattern with a highlighted HTML span
const wrapPattern = ( pattern , className ) => {
regexStr = regexStr . replace ( pattern , match => ` <span class=" ${ className } "> ${ match } </span> ` ) ;
} ;
// Apply highlighting patterns
wrapPattern ( patterns . brackets , 'regex-brackets' ) ;
wrapPattern ( patterns . quantifiers , 'regex-quantifier' ) ;
wrapPattern ( patterns . operators , 'regex-operator' ) ;
wrapPattern ( patterns . specialChars , 'regex-special' ) ;
wrapPattern ( patterns . flags , 'regex-flags' ) ;
wrapPattern ( patterns . delimiters , 'regex-delimiter' ) ;
return ` <span class="regex-highlight"> ${ regexStr } </span> ` ;
2024-05-09 23:30:18 +02:00
}
2024-05-22 23:52:35 +02:00
/ * *
* Confirms if the user wants to overwrite an existing data object ( like character , world info , etc ) if one exists .
* If no data with the name exists , this simply returns true .
*
* @ param { string } type - The type of the check ( "World Info" , "Character" , etc )
* @ param { string [ ] } existingNames - The list of existing names to check against
* @ param { string } name - The new name
* @ param { object } options - Optional parameters
* @ param { boolean } [ options . interactive = false ] - Whether to show a confirmation dialog when needing to overwrite an existing data object
* @ param { string } [ options . actionName = 'overwrite' ] - The action name to display in the confirmation dialog
* @ param { ( existingName : string ) => void } [ options . deleteAction = null ] - Optional action to execute wen deleting an existing data object on overwrite
* @ returns { Promise < boolean > } True if the user confirmed the overwrite or there is no overwrite needed , false otherwise
* /
export async function checkOverwriteExistingData ( type , existingNames , name , { interactive = false , actionName = 'Overwrite' , deleteAction = null } = { } ) {
const existing = existingNames . find ( x => equalsIgnoreCaseAndAccents ( x , name ) ) ;
if ( ! existing ) {
return true ;
}
2024-06-26 05:35:41 +02:00
const overwrite = interactive && await Popup . show . confirm ( ` ${ type } ${ actionName } ` , ` <p>A ${ type . toLowerCase ( ) } with the same name already exists:<br /> ${ existing } </p>Do you want to overwrite it? ` ) ;
2024-05-22 23:52:35 +02:00
if ( ! overwrite ) {
toastr . warning ( ` ${ type } ${ actionName . toLowerCase ( ) } cancelled. A ${ type . toLowerCase ( ) } with the same name already exists:<br /> ${ existing } ` , ` ${ type } ${ actionName } ` , { escapeHtml : false } ) ;
return false ;
}
toastr . info ( ` Overwriting Existing ${ type } :<br /> ${ existing } ` , ` ${ type } ${ actionName } ` , { escapeHtml : false } ) ;
// If there is an action to delete the existing data, do it, as the name might be slightly different so file name would not be the same
if ( deleteAction ) {
deleteAction ( existing ) ;
}
return true ;
}
2024-05-27 03:35:03 +02:00
/ * *
* Generates a free name by appending a counter to the given name if it already exists in the list
*
* @ param { string } name - The original name to check for existence in the list
* @ param { string [ ] } list - The list of names to check for existence
* @ param { ( n : number ) => string } [ numberFormatter = ( n ) => ` # ${ n } ` ] - The function used to format the counter
* @ returns { string } The generated free name
* /
export function getFreeName ( name , list , numberFormatter = ( n ) => ` # ${ n } ` ) {
if ( ! list . includes ( name ) ) {
return name ;
}
let counter = 1 ;
while ( list . includes ( ` ${ name } # ${ counter } ` ) ) {
counter ++ ;
}
return ` ${ name } ${ numberFormatter ( counter ) } ` ;
}
2024-08-05 00:09:50 +02:00
/ * *
* Toggles the visibility of a drawer by changing the display style of its content .
* This function skips the usual drawer animation .
*
* @ param { HTMLElement } drawer - The drawer element to toggle
* @ param { boolean } [ expand = true ] - Whether to expand or collapse the drawer
* /
export function toggleDrawer ( drawer , expand = true ) {
/** @type {HTMLElement} */
const icon = drawer . querySelector ( '.inline-drawer-icon' ) ;
/** @type {HTMLElement} */
const content = drawer . querySelector ( '.inline-drawer-content' ) ;
if ( expand ) {
icon . classList . remove ( 'up' , 'fa-circle-chevron-up' ) ;
icon . classList . add ( 'down' , 'fa-circle-chevron-down' ) ;
content . style . display = 'block' ;
} else {
icon . classList . remove ( 'down' , 'fa-circle-chevron-down' ) ;
icon . classList . add ( 'up' , 'fa-circle-chevron-up' ) ;
content . style . display = 'none' ;
}
// Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height
2024-09-05 19:00:45 +02:00
if ( ! CSS . supports ( 'field-sizing' , 'content' ) ) {
2024-09-05 18:48:21 +02:00
content . querySelectorAll ( 'textarea.autoSetHeight' ) . forEach ( resetScrollHeight ) ;
}
2024-08-05 00:09:50 +02:00
}
2024-08-05 23:29:24 +02:00
2024-07-16 15:28:03 +02:00
export async function fetchFaFile ( name ) {
const style = document . createElement ( 'style' ) ;
style . innerHTML = await ( await fetch ( ` /css/ ${ name } ` ) ) . text ( ) ;
document . head . append ( style ) ;
const sheet = style . sheet ;
style . remove ( ) ;
2024-09-06 16:07:50 +02:00
return [ ... sheet . cssRules ]
. filter ( rule => rule . style ? . content )
2024-09-07 20:19:33 +02:00
. map ( rule => rule . selectorText . split ( /,\s*/ ) . map ( selector => selector . split ( '::' ) . shift ( ) . slice ( 1 ) ) )
2024-09-06 16:07:50 +02:00
;
2024-07-16 15:28:03 +02:00
}
export async function fetchFa ( ) {
return [ ... new Set ( ( await Promise . all ( [
fetchFaFile ( 'fontawesome.min.css' ) ,
2024-07-09 19:28:06 +02:00
] ) ) . flat ( ) ) ] ;
2024-07-16 15:28:03 +02:00
}
/ * *
* Opens a popup with all the available Font Awesome icons and returns the selected icon ' s name .
* @ prop { string [ ] } customList A custom list of Font Awesome icons to use instead of all available icons .
* @ returns { Promise < string > } The icon name ( fa - pencil ) or null if cancelled .
* /
export async function showFontAwesomePicker ( customList = null ) {
const faList = customList ? ? await fetchFa ( ) ;
2024-07-09 19:28:06 +02:00
const fas = { } ;
const dom = document . createElement ( 'div' ) ; {
2024-07-19 00:28:24 +02:00
dom . classList . add ( 'faPicker-container' ) ;
2024-07-09 19:28:06 +02:00
const search = document . createElement ( 'div' ) ; {
2024-07-19 00:28:24 +02:00
search . classList . add ( 'faQuery-container' ) ;
2024-07-09 19:28:06 +02:00
const qry = document . createElement ( 'input' ) ; {
qry . classList . add ( 'text_pole' ) ;
qry . classList . add ( 'faQuery' ) ;
qry . type = 'search' ;
qry . placeholder = 'Filter icons' ;
qry . autofocus = true ;
2024-08-05 05:03:46 +02:00
const qryDebounced = debounce ( ( ) => {
2024-09-07 20:19:33 +02:00
const result = faList . filter ( fa => fa . find ( className => className . includes ( qry . value . toLowerCase ( ) ) ) ) ;
2024-07-09 19:28:06 +02:00
for ( const fa of faList ) {
if ( ! result . includes ( fa ) ) {
fas [ fa ] . classList . add ( 'hidden' ) ;
} else {
fas [ fa ] . classList . remove ( 'hidden' ) ;
}
}
} ) ;
2024-07-17 20:47:42 +02:00
qry . addEventListener ( 'input' , ( ) => qryDebounced ( ) ) ;
2024-07-09 19:28:06 +02:00
search . append ( qry ) ;
}
dom . append ( search ) ;
}
const grid = document . createElement ( 'div' ) ; {
grid . classList . add ( 'faPicker' ) ;
for ( const fa of faList ) {
const opt = document . createElement ( 'div' ) ; {
fas [ fa ] = opt ;
opt . classList . add ( 'menu_button' ) ;
opt . classList . add ( 'fa-solid' ) ;
2024-09-06 16:07:50 +02:00
opt . classList . add ( fa [ 0 ] ) ;
2024-09-07 20:19:33 +02:00
opt . title = fa . map ( it => it . slice ( 3 ) ) . join ( ', ' ) ;
2024-07-09 19:28:06 +02:00
opt . dataset . result = POPUP _RESULT . AFFIRMATIVE . toString ( ) ;
2024-09-06 16:07:50 +02:00
opt . addEventListener ( 'click' , ( ) => value = fa [ 0 ] ) ;
2024-07-09 19:28:06 +02:00
grid . append ( opt ) ;
}
}
dom . append ( grid ) ;
}
}
2024-07-16 00:25:48 +02:00
let value = '' ;
2024-07-17 20:47:42 +02:00
const picker = new Popup ( dom , POPUP _TYPE . TEXT , null , { allowVerticalScrolling : true , okButton : 'No Icon' , cancelButton : 'Cancel' } ) ;
2024-07-09 19:28:06 +02:00
await picker . show ( ) ;
if ( picker . result == POPUP _RESULT . AFFIRMATIVE ) {
return value ;
}
return null ;
}
2024-09-29 03:20:01 +02:00
/ * *
* Finds a character by name , with optional filtering and precedence for avatars
* @ param { object } [ options = { } ] - The options for the search
* @ param { string ? } [ options . name = null ] - The name to search for
* @ param { boolean } [ options . allowAvatar = true ] - Whether to allow searching by avatar
* @ param { boolean } [ options . insensitive = true ] - Whether the search should be case insensitive
* @ param { string [ ] ? } [ options . filteredByTags = null ] - Tags to filter characters by
* @ param { boolean } [ options . preferCurrentChar = true ] - Whether to prefer the current character ( s )
* @ param { boolean } [ options . quiet = false ] - Whether to suppress warnings
* @ returns { any ? } - The found character or null if not found
* /
export function findChar ( { name = null , allowAvatar = true , insensitive = true , filteredByTags = null , preferCurrentChar = true , quiet = false } = { } ) {
2024-09-29 17:00:28 +02:00
const matches = ( char ) => ! name || ( allowAvatar && char . avatar === name ) || ( insensitive ? equalsIgnoreCaseAndAccents ( char . name , name ) : char . name === name ) ;
2024-09-29 03:20:01 +02:00
// Filter characters by tags if provided
let filteredCharacters = characters ;
if ( filteredByTags ) {
filteredCharacters = characters . filter ( char => {
const charTags = getTagsList ( char . avatar , false ) ;
return filteredByTags . every ( tagName => charTags . some ( x => x . name == tagName ) ) ;
} ) ;
}
// Get the current character(s)
/** @type {any[]} */
const currentChars = selected _group ? groups . find ( group => group . id === selected _group ) ? . members . map ( member => filteredCharacters . find ( char => char . avatar === member ) )
2024-09-29 13:38:15 +02:00
: filteredCharacters . filter ( char => characters [ this _chid ] ? . avatar === char . avatar ) ;
2024-09-29 03:20:01 +02:00
// If we have a current char and prefer it, return that if it matches
if ( preferCurrentChar ) {
const preferredCharSearch = currentChars . filter ( matches ) ;
if ( preferredCharSearch . length > 1 ) {
2024-09-29 17:00:28 +02:00
if ( ! quiet ) toastr . warning ( 'Multiple characters found for given conditions.' ) ;
else console . warn ( 'Multiple characters found for given conditions. Returning the first match.' ) ;
2024-09-29 03:20:01 +02:00
}
if ( preferredCharSearch . length ) {
return preferredCharSearch [ 0 ] ;
}
}
// If allowAvatar is true, search by avatar first
if ( allowAvatar && name ) {
const characterByAvatar = filteredCharacters . find ( char => char . avatar === name ) ;
if ( characterByAvatar ) {
return characterByAvatar ;
}
}
// Search for matching characters by name
const matchingCharacters = name ? filteredCharacters . filter ( matches ) : filteredCharacters ;
if ( matchingCharacters . length > 1 ) {
2024-09-29 17:00:28 +02:00
if ( ! quiet ) toastr . warning ( 'Multiple characters found for given conditions.' ) ;
else console . warn ( 'Multiple characters found for given conditions. Returning the first match.' ) ;
2024-09-29 03:20:01 +02:00
}
return matchingCharacters [ 0 ] || null ;
}
/ * *
* Gets the index of a character based on the character object
* @ param { object } char - The character object to find the index for
* @ throws { Error } If the character is not found
* @ returns { number } The index of the character in the characters array
* /
export function getCharIndex ( char ) {
if ( ! char ) throw new Error ( 'Character is undefined' ) ;
const index = characters . findIndex ( c => c . avatar === char . avatar ) ;
if ( index === - 1 ) throw new Error ( ` Character not found: ${ char . avatar } ` ) ;
return index ;
}