Base Brazen Resource

Base library for my scripts

当前为 2020-12-07 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/375557/877825/Base%20Brazen%20Resource.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Base Brazen Resource
// @namespace    brazen
// @version      3.3.0
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Base library for my scripts
// @run-at  	 document-end
// ==/UserScript==

const CONFIG_TYPE_CHECKBOXES_GROUP = 'checkboxes'
const CONFIG_TYPE_FLAG = 'flag'
const CONFIG_TYPE_NUMBER = 'number'
const CONFIG_TYPE_RADIOS_GROUP = 'radios'
const CONFIG_TYPE_RANGE = 'range'
const CONFIG_TYPE_RULESET = 'ruleset'
const CONFIG_TYPE_TEXT = 'text'

const REGEX_LINE_BREAK = /\r?\n/g
const REGEX_PRESERVE_NUMBERS = /[^0-9]/g

class ChildObserver
{
    /**
     * @callback observerOnMutation
     * @param {NodeList} nodes
     */

    /**
     * @return {ChildObserver}
     */
    static create ()
    {
        return new ChildObserver
    }

    /**
     * ChildObserver constructor
     */
    constructor ()
    {
        this._node = null
        this._observer = null
        this._onNodesAdded = null
        this._onNodesRemoved = null
    }

    /**
     * @return {ChildObserver}
     * @private
     */
    _observeNodes ()
    {
        this._observer.observe(this._node, {childList: true})
        return this
    }

    /**
     * Attach an observer to the specified node(s)
     * @param {Node} node
     * @returns {ChildObserver}
     */
    observe (node)
    {
        this._node = node
        this._observer = new MutationObserver((mutations) => {
            for (let mutation of mutations) {
                if (mutation.addedNodes.length && this._onNodesAdded !== null) {
                    this._onNodesAdded(
                        mutation.addedNodes,
                        mutation.previousSibling,
                        mutation.nextSibling,
                        mutation.target,
                    )
                }
                if (mutation.removedNodes.length && this._onNodesRemoved !== null) {
                    this._onNodesRemoved(
                        mutation.removedNodes,
                        mutation.previousSibling,
                        mutation.nextSibling,
                        mutation.target,
                    )
                }
            }
        })
        return this._observeNodes()
    }

    /**
     * @param {observerOnMutation} eventHandler
     * @returns {ChildObserver}
     */
    onNodesAdded (eventHandler)
    {
        this._onNodesAdded = eventHandler
        return this
    }

    /**
     * @param {observerOnMutation} eventHandler
     * @returns {ChildObserver}
     */
    onNodesRemoved (eventHandler)
    {
        this._onNodesRemoved = eventHandler
        return this
    }

    pauseObservation ()
    {
        this._observer.disconnect()
    }

    resumeObservation ()
    {
        this._observeNodes()
    }
}

class ConfigurationManager
{
    /**
     * @typedef {{title: string, type: string, element: null|JQuery, value: *, maximum: int, minimum: int, options: string[], helpText: string, onEncode: null|Function, onDecode:
     *     null|Function}} ConfigurationField
     */

    /**
     * @param {BrazenUIGenerator} uiGenerator
     * @return {ConfigurationManager}
     */
    static create (uiGenerator)
    {
        return new ConfigurationManager(uiGenerator)
    }

    constructor (uiGenerator)
    {
        /**
         * @type {{}}
         * @private
         */
        this._config = {}

        /**
         * @type {LocalStore}
         * @private
         */
        this._localStore = null

        /**
         * @type BrazenUIGenerator
         * @private
         */
        this._uiGen = uiGenerator
    }

    /**
     * @param {string} type
     * @param {string} name
     * @param {*} value
     * @param {string} helpText
     * @return ConfigurationField
     * @private
     */
    _createField (type, name, value, helpText)
    {
        let field
        if (typeof this._config[name] === 'undefined') {
            field = {
                element: null,
                helpText: helpText,
                title: name,
                type: type,
                value: value,
            }
            this._config[Utilities.toKebabCase(name)] = field
        } else {
            field = this._config[name]
            field.helpText = helpText
            field.value = value
        }
        return field
    }

    /**
     * @param {boolean} ignoreIfDefaultsSet
     * @private
     */
    _syncLocalStore (ignoreIfDefaultsSet)
    {
        let storeObject = this._localStore.get()
        if (!ignoreIfDefaultsSet || !this._localStore.wereDefaultsSet()) {
            for (let key in this._config) {
                this._config[key].value = storeObject[key]
            }
            this.updateInterface()
        }
        return this
    }

    /**
     * @return {{}}
     * @private
     */
    _toStoreObject ()
    {
        let storeObject = {}
        for (let key in this._config) {
            storeObject[key] = this._config[key].value
        }
        return storeObject
    }

    createElement (name)
    {
        let field = this.getField(name)
        let inputGroup
        switch (field.type) {
            case CONFIG_TYPE_CHECKBOXES_GROUP:
                inputGroup = this._uiGen.createFormCheckBoxesGroupSection(field.title, field.options, field.helpText)
                field.element = inputGroup
                break
            case CONFIG_TYPE_FLAG:
                inputGroup = this._uiGen.createFormInputGroup(field.title, 'checkbox', field.helpText)
                field.element = inputGroup.find('input')
                break
            case CONFIG_TYPE_NUMBER:
                inputGroup = this._uiGen.createFormInputGroup(field.title, 'number', field.helpText).attr('min', field.minimum).attr('max', field.maximum)
                field.element = inputGroup.find('input')
                break
            case CONFIG_TYPE_RADIOS_GROUP:
                inputGroup = this._uiGen.createFormRadiosGroupSection(field.title, field.options, field.helpText)
                field.element = inputGroup
                break
            case CONFIG_TYPE_RANGE:
                inputGroup = this._uiGen.createFormRangeInputGroup(field.title, 'number', field.minimum, field.maximum, field.helpText)
                field.element = inputGroup.find('input')
                break
            case CONFIG_TYPE_RULESET:
                inputGroup = this._uiGen.createFormTextAreaGroup(field.title, 2, field.helpText)
                field.element = inputGroup.find('textarea')
                break
            case CONFIG_TYPE_TEXT:
                inputGroup = this._uiGen.createFormInputGroup(field.title, 'text', field.helpText)
                field.element = inputGroup.find('input')
        }
        return inputGroup
    }

    initialize (scriptPrefix)
    {
        this._localStore = new LocalStore(scriptPrefix + 'settings', this._toStoreObject())
        this._localStore.onChange(() => this.updateInterface())

        return this._syncLocalStore(true)
    }

    addCheckboxesGroup (name, keyValuePairs, helpText)
    {
        let field = this._createField(CONFIG_TYPE_CHECKBOXES_GROUP, name, [], helpText)
        field.options = keyValuePairs
        return this
    }

    addFlagField (name, helpText)
    {
        this._createField(CONFIG_TYPE_FLAG, name, false, helpText)
        return this
    }

    addNumberField (name, minimum, maximum, helpText)
    {
        let field = this._createField(CONFIG_TYPE_NUMBER, name, minimum, helpText)
        field.minimum = minimum
        field.maximum = maximum
        return this
    }

    addRadiosGroup (name, keyValuePairs, helpText)
    {
        let field = this._createField(CONFIG_TYPE_RADIOS_GROUP, name, keyValuePairs[0][1], helpText)
        field.options = keyValuePairs
        return this
    }

    addRangeField (name, minimum, maximum, helpText)
    {
        let field = this._createField(CONFIG_TYPE_RANGE, name, {minimum: minimum, maximum: minimum}, helpText)
        field.minimum = minimum
        field.maximum = maximum
        return this
    }

    addRulesetField (name, helpText, onEncode = null, onDecode = null)
    {
        let field = this._createField(CONFIG_TYPE_RULESET, name, [], helpText)
        field.onEncode = onEncode ?? field.onEncode
        field.onDecode = onDecode ?? field.onDecode
        return this
    }

    addTextField (name, helpText)
    {
        this._createField(CONFIG_TYPE_TEXT, name, '', helpText)
        return this
    }

    /**
     * @param {string} name
     * @return {ConfigurationField}
     */
    getField (name)
    {
        let field = this._config[Utilities.toKebabCase(name)]
        if (field) {
            return field
        }
        throw new Error('Field named "' + name + '" could not be found')
    }

    getValue (name)
    {
        return this.getField(name).value
    }

    revertChanges ()
    {
        return this._syncLocalStore(false)
    }

    save ()
    {
        this.update()._localStore.save(this._toStoreObject())
        return this
    }

    update ()
    {
        let field, value
        for (let fieldName in this._config) {

            field = this._config[fieldName]
            if (field.element) {

                switch (field.type) {
                    case CONFIG_TYPE_CHECKBOXES_GROUP:
                        field.value = []
                        field.element.find('input:checked').each((index, element) => {
                            field.value.push($(element).attr('data-value'))
                        })
                        break
                    case CONFIG_TYPE_FLAG:
                        field.value = field.element.prop('checked')
                        break
                    case CONFIG_TYPE_NUMBER:
                        field.value = parseInt(field.element.val())
                        break
                    case CONFIG_TYPE_RADIOS_GROUP:
                        field.value = field.element.find('input:checked').attr('data-value')
                        break
                    case CONFIG_TYPE_RANGE:
                        field.value = {
                            minimum: field.element.first().val(),
                            maximum: field.element.last().val(),
                        }
                        break
                    case CONFIG_TYPE_RULESET:
                        value = Utilities.trimAndKeepNonEmptyStrings(field.element.val().split(REGEX_LINE_BREAK))
                        field.value = field.onDecode ? field.onDecode(value, field) : value
                        break
                    default:
                        field.value = field.element.val()
                }
            }
        }
        return this
    }

    updateInterface ()
    {
        let elements, field, value
        for (let fieldName in this._config) {

            field = this._config[fieldName]
            if (field.element) {

                switch (field.type) {
                    case CONFIG_TYPE_CHECKBOXES_GROUP:
                        elements = field.element.find('input')
                        for (let key of field.value) {
                            elements.filter('[data-value="' + key + '"]').prop('checked', true)
                        }
                        break
                    case CONFIG_TYPE_FLAG:
                        field.element.prop('checked', field.value)
                        break
                    case CONFIG_TYPE_NUMBER:
                        field.element.val(field.value)
                        break
                    case CONFIG_TYPE_RADIOS_GROUP:
                        field.element.find('input[data-value="' + field.value + '"]').prop('checked', true).trigger('change')
                        break
                    case CONFIG_TYPE_RANGE:
                        field.element.first().val(field.value.minimum)
                        field.element.last().val(field.value.maximum)
                        break
                    case CONFIG_TYPE_RULESET:
                        value = field.onEncode ? field.onEncode(field.value) : field.value
                        field.element.val(value.join('\n'))
                        break
                    default:
                        field.element.val(field.value)
                }
            }
        }
        return this
    }
}

class LocalStore
{
    /**
     * @callback storeEventHandler
     * @param {Object} store
     */

    /**
     * @param {string} key
     * @param {Object} defaults
     */
    constructor (key, defaults)
    {
        /**
         * @type {Object}
         * @private
         */
        this._defaults = defaults

        /**
         * @type {boolean}
         * @private
         */
        this._defaultsSet = false

        /**
         * @type {string}
         * @private
         */
        this._key = key

        // Events

        /**
         * @type {storeEventHandler}
         */
        this._onChange = null
    }

    _handleOnChange ()
    {
        if (this._onChange !== null) {
            this._onChange(this.get())
        }
    }

    /**
     * @return {LocalStore}
     */
    delete ()
    {
        window.localStorage.removeItem(this._key)
        return this
    }

    /**
     * @param {string} filename
     */
    exportToFile (filename)
    {
        let linkElement = document.createElement('a')
        let file = new Blob([Utilities.objectToJSON(this.get())], {type: 'application/json'})
        linkElement.href = URL.createObjectURL(file)
        linkElement.download = filename
        linkElement.click()

        URL.revokeObjectURL(linkElement.href)
        linkElement.remove()
    }

    /**
     * @return {*}
     */
    get ()
    {
        this._defaultsSet = false
        let storedStore = window.localStorage.getItem(this._key)
        return storedStore === null ? this.restoreDefaults() : Utilities.objectFromJSON(storedStore)
    }

    importFromFile (file)
    {
    }

    /**
     * @param {storeEventHandler} handler
     * @return {LocalStore}
     */
    onChange (handler)
    {
        this._onChange = handler
        return this
    }

    /**
     * @return {Object}
     */
    restoreDefaults ()
    {
        this._defaultsSet = true
        this.save(this._defaults)
        return this._defaults
    }

    /**
     * @param {Object} data
     * @return {LocalStore}
     */
    save (data)
    {
        window.localStorage.setItem(this._key, Utilities.objectToJSON(data))
        this._handleOnChange()
        return this
    }

    /**
     * @return {boolean}
     */
    wereDefaultsSet ()
    {
        return this._defaultsSet
    }
}

class Paginator
{
    /**
     * @callback PaginatorAfterPaginationEventHandler
     * @param {Paginator} paginator
     */

    /**
     * @callback PaginatorGetPageNoFromUrlHandler
     * @param {string} pageUrl
     * @param {Paginator} paginator
     */

    /**
     * @callback PaginatorGetPageUrlFromPageNoHandler
     * @param {number} pageNo
     * @param {Paginator} paginator
     */

    /**
     * @callback PaginatorGetPaginationElementForPageNoHandler
     * @param {number} pageNo
     * @param {Paginator} paginator
     */

    /**
     * @param {JQuery} paginationWrapper
     * @param {JQuery.Selector} listSelector
     * @param {JQuery.Selector} itemClassesSelector
     * @param {string } lastPageUrl
     * @return {Paginator}
     */
    static create (paginationWrapper, listSelector, itemClassesSelector, lastPageUrl)
    {
        return (new Paginator).configure(paginationWrapper, listSelector, itemClassesSelector, lastPageUrl)
    }

    /**
     *
     */
    constructor ()
    {
        /**
         * @type {number}
         * @private
         */
        this._currentPageNo = 0

        /**
         * @type {JQuery.Selector}
         * @private
         */
        this._itemClassesSelector = ''

        /**
         * @type {number}
         * @private
         */
        this._lastPageNo = 0

        /**
         * @type {string}
         * @private
         */
        this._lastPageUrl = ''

        /**
         * @type {JQuery.Selector}
         * @private
         */
        this._listSelector = ''

        /**
         * @type {number}
         * @private
         */
        this._paginatedPageNo = 0

        /**
         * @type {JQuery}
         * @private
         */
        this._paginationWrapper = null

        /**
         * @type {JQuery}
         * @private
         */
        this._targetElement = null

        // Events and callbacks

        /**
         * @type {PaginatorAfterPaginationEventHandler}
         * @private
         */
        this._onAfterPagination = null

        /**
         * @type {PaginatorGetPageNoFromUrlHandler}
         * @private
         */
        this._onGetPageNoFromUrl = null

        /**
         * @type {PaginatorGetPageUrlFromPageNoHandler}
         * @private
         */
        this._onGetPageUrlFromPageNo = null

        /**
         * @type {PaginatorGetPaginationElementForPageNoHandler}
         * @private
         */
        this._onGetPaginationElementForPageNo = null
    }

    _conformUIToNewPaginatedState ()
    {
        let currentPageElement = this.getPaginationElementForPageNo(this._currentPageNo)

        // Determine whether next page's pagination element exists

        let nextPageElement = this.getPaginationElementForPageNo(this._currentPageNo + 1)

        // Delete pagination element of paginated page

        // Mutate current page no element to show paginated page numbers

        currentPageElement.text(this._currentPageNo + '-' + this._paginatedPageNo)
    }

    /**
     * @param {number} threshold
     * @param {number} limit
     * @param {number} iteration
     * @return {number}
     * @private
     */
    _loadAndParseNextPage (threshold, limit, iteration = 0)
    {
        let lastPageHasNotBeenReached = this._paginatedPageNo < this._lastPageNo
        let paginationLimitHasNotBeenMet = (limit <= 0 || (this._paginatedPageNo - this._currentPageNo <= limit))
        let compliantItemsAreLessThanTheThreshold = this._targetElement.find(this._itemClassesSelector + ':visible').length < threshold

        if (lastPageHasNotBeenReached && paginationLimitHasNotBeenMet && compliantItemsAreLessThanTheThreshold) {

            this._sandbox.load(this.getPageUrlFromPageNo(++this._paginatedPageNo) + ' ' + this._listSelector, '', () => {
                this._sandbox.find(this._itemClassesSelector).insertAfter(this._targetElement.find(this._itemClassesSelector + ':last'))
                this._sandbox.empty()
                Utilities.callEventHandler(this.onAfterPagination, [this])
            })

            iteration = this._loadAndParseNextPage(threshold, limit, iteration + 1)
        }
        return iteration
    }

    /**
     * @param {JQuery} paginationWrapper
     * @param {JQuery.Selector} listSelector
     * @param {JQuery.Selector} itemClassesSelector
     * @param {string } lastPageUrl
     * @return {Paginator}
     */
    configure (paginationWrapper, listSelector, itemClassesSelector, lastPageUrl)
    {
        this._lastPageUrl = lastPageUrl
        this._listSelector = listSelector
        this._itemClassesSelector = itemClassesSelector
        this._paginationWrapper = paginationWrapper
        return this
    }

    getCurrentPageNo ()
    {
        return this._currentPageNo
    }

    getLastPageNo ()
    {
        return this._lastPageNo
    }

    /**
     * @param {string} pageUrl
     * @return {number}
     */
    getPageNoFromUrl (pageUrl)
    {
        return Utilities.callEventHandlerOrFail('onGetPageNoFromUrl', this._onGetPageNoFromUrl, [pageUrl, this])
    }

    /**
     * @param {number} pageNo
     * @return {string}
     */
    getPageUrlFromPageNo (pageNo)
    {
        return Utilities.callEventHandlerOrFail('onGetPageUrlFromPageNo', this._onGetPageUrlFromPageNo, [pageNo, this])
    }

    /**
     * @param {number} pageNo
     * @return {JQuery}
     */
    getPaginationElementForPageNo (pageNo)
    {
        return Utilities.callEventHandlerOrFail('onGetPaginationElementForPageNo', this._onGetPaginationElementForPageNo, [pageNo, this])
    }

    getPaginatedPageNo ()
    {
        return this._paginatedPageNo
    }

    getPaginationWrapper ()
    {
        return this._paginationWrapper
    }

    initialize ()
    {
        this._currentPageNo = this.getPageNoFromUrl(window.location.href)
        this._lastPageNo = this.getPageNoFromUrl(this._lastPageUrl)
        this._paginatedPageNo = this._currentPageNo
        this._sandbox = $('<div id="brazen-paginator-sandbox" hidden/>').appendTo('body')
        this._targetElement = $(this._listSelector + ':first')
        return this
    }

    /**
     * @param {PaginatorAfterPaginationEventHandler} handler
     * @return {this}
     */
    onAfterPagination (handler)
    {
        this._onAfterPagination = handler
        return this
    }

    /**
     * @param {PaginatorGetPageNoFromUrlHandler} handler
     * @return {this}
     */
    onGetPageNoFromUrl (handler)
    {
        this._onGetPageNoFromUrl = handler
        return this
    }

    /**
     * @param {PaginatorGetPageUrlFromPageNoHandler} handler
     * @return {this}
     */
    onGetPageUrlFromPageNo (handler)
    {
        this._onGetPageUrlFromPageNo = handler
        return this
    }

    /**
     * @param {PaginatorGetPaginationElementForPageNoHandler} handler
     * @return {this}
     */
    onGetPaginationElementForPageNo (handler)
    {
        this._onGetPaginationElementForPageNo = handler
        return this
    }

    run (threshold, limit)
    {
        if (this._paginationWrapper.length && threshold) {
            if (this._loadAndParseNextPage(threshold, limit)) {
                this._conformUIToNewPaginatedState()
            }
        }
        return this
    }
}

class SelectorGenerator
{
    /**
     * @param {string} selectorPrefix
     */
    constructor (selectorPrefix)
    {
        /**
         * @type {string}
         * @private
         */
        this._prefix = selectorPrefix
    }

    /**
     * @param {string} selector
     * @return {string}
     */
    getSelector (selector)
    {
        return this._prefix + selector
    }

    /**
     * @param {string} settingName
     * @return {string}
     */
    getSettingsInputSelector (settingName)
    {
        return this.getSelector(Utilities.toKebabCase(settingName) + '-setting')
    }

    /**
     * @param {string} settingName
     * @param {boolean} getMinInputSelector
     * @return {string}
     */
    getSettingsRangeInputSelector (settingName, getMinInputSelector)
    {
        return this.getSelector(Utilities.toKebabCase(settingName) + (getMinInputSelector ? '-min' : '-max') + '-setting')
    }

    /**
     * @param {string} statisticType
     * @return {string}
     */
    getStatLabelSelector (statisticType)
    {
        return this.getSelector(Utilities.toKebabCase(statisticType) + '-stat')
    }
}

class StatisticsRecorder
{
    /**
     * @param {string} selectorPrefix
     */
    constructor (selectorPrefix)
    {
        /**
         * @type {SelectorGenerator}
         * @private
         */
        this._selectorGenerator = new SelectorGenerator(selectorPrefix)

        /**
         * @type {{Total: number}}
         * @private
         */
        this._statistics = {Total: 0}
    }

    /**
     * @param {string} statisticType
     * @param {boolean} validationResult
     * @param {number} value
     */
    record (statisticType, validationResult, value = 1)
    {
        if (!validationResult) {
            if (typeof this._statistics[statisticType] !== 'undefined') {
                this._statistics[statisticType] += value
            } else {
                this._statistics[statisticType] = value
            }
            this._statistics.Total += value
        }
    }

    reset ()
    {
        for (const statisticType in this._statistics) {
            this._statistics[statisticType] = 0
        }
    }

    updateUI ()
    {
        let label, labelSelector

        for (const statisticType in this._statistics) {
            labelSelector = this._selectorGenerator.getStatLabelSelector(statisticType)
            label = document.getElementById(labelSelector)
            if (label !== null) {
                label.textContent = this._statistics[statisticType]
            }
        }
    }
}

class Utilities
{
    /**
     * @param {string[]} words
     * @return {RegExp}
     */
    static buildWholeWordMatchingRegex (words)
    {
        let patternedWords = []
        for (let i = 0; i < words.length; i++) {
            patternedWords.push('\\b' + words[i] + '\\b')
        }
        return new RegExp('(' + patternedWords.join('|') + ')', 'gi')
    }

    static callEventHandler (handler, parameters)
    {
        return handler ? handler(...parameters) : null
    }

    static callEventHandlerOrFail (name, handler, parameters)
    {
        if (handler) {
            return handler(...parameters)
        }
        throw new Error('Callback "' + name + '" must be defined.')
    }

    /**
     * @param {string} json
     * @return {Object}
     */
    static objectFromJSON (json)
    {
        /** @type {{arrays: Object, objects: Object, properties: Object}} */
        let parsedJSON = JSON.parse(json)
        let arrayObject = {}
        let result = {}

        for (let property in parsedJSON.arrays) {
            arrayObject = JSON.parse(parsedJSON.arrays[property])
            result[property] = []

            for (let key in arrayObject) {
                result[property].push(arrayObject[key])
            }
        }
        for (let property in parsedJSON.objects) {
            result[property] = Utilities.objectFromJSON(parsedJSON.objects[property])
        }
        for (let property in parsedJSON.properties) {
            result[property] = parsedJSON.properties[property]
        }
        return result
    }

    /**
     * @param {Object} object
     * @return {string}
     */
    static objectToJSON (object)
    {
        let arrayToObject
        let json = {arrays: {}, objects: {}, properties: {}}
        for (let property in object) {
            if (typeof object[property] === 'object') {
                if (Array.isArray(object[property])) {
                    arrayToObject = {}
                    for (let key in object[property]) {
                        arrayToObject[key] = object[property][key]
                    }
                    json.arrays[property] = JSON.stringify(arrayToObject)
                } else {
                    json.objects[property] = Utilities.objectToJSON(object[property])
                }
            } else {
                json.properties[property] = object[property]
            }
        }
        return JSON.stringify(json)
    }

    /**
     * @param milliseconds
     * @return {Promise<*>}
     */
    static sleep (milliseconds)
    {
        return new Promise(resolve => setTimeout(resolve, milliseconds))
    }

    /**
     * @param {string} text
     * @return {string}
     */
    static toKebabCase (text)
    {
        return text.toLowerCase().replaceAll(' ', '-')
    }

    /**
     * @param {string[]} strings
     */
    static trimAndKeepNonEmptyStrings (strings)
    {
        let nonEmptyStrings = []
        for (let string of strings) {
            string = string.trim()
            if (string !== '') {
                nonEmptyStrings.push(string)
            }
        }
        return nonEmptyStrings
    }
}

class Validator
{
    static iFramesRemover ()
    {
        GM_addStyle(' iframe { display: none !important; } ')
    }

    /**
     * @param {StatisticsRecorder} statisticsRecorder
     */
    constructor (statisticsRecorder)
    {
        /**
         * @type {StatisticsRecorder}
         * @private
         */
        this._statisticsRecorder = statisticsRecorder
    }

    /**
     * @param {string} text
     * @param {Object} rules
     * @return {string}
     */
    sanitize (text, rules)
    {
        for (const substitute in rules) {
            text = text.replace(rules[substitute], substitute)
        }
        return text.trim()
    }

    /**
     * @param {JQuery} textNode
     * @param {Object} rules
     * @return {Validator}
     */
    sanitizeTextNode (textNode, rules)
    {
        textNode.text(this.sanitize(textNode.text(), rules))
        return this
    }

    /**
     * @param {string} selector
     * @param {Object} rules
     * @return {Validator}
     */
    sanitizeNodeOfSelector (selector, rules)
    {
        let node = $(selector)
        if (node.length) {
            let sanitizedText = this.sanitize(node.text(), rules)
            node.text(sanitizedText)
            document.title = sanitizedText
        }
        return this
    }

    /**
     * @param {string} name
     * @param {JQuery} item
     * @param {string} selector
     * @return {boolean}
     */
    validateNodeExistence (name, item, selector)
    {
        let validationCheck = item.find(selector).length > 0
        this._statisticsRecorder.record(name, validationCheck)

        return validationCheck
    }

    /**
     * @param {string} name
     * @param {JQuery} item
     * @param {string} selector
     * @return {boolean}
     */
    validateNodeNonExistence (name, item, selector)
    {
        let validationCheck = item.find(selector).length === 0
        this._statisticsRecorder.record(name, validationCheck)

        return validationCheck
    }

    /**
     * @param {string} name
     * @param {number} value
     * @param {number[]} bounds
     * @return {boolean}
     */
    validateRange (name, value, bounds)
    {
        let validationCheck = true

        if (bounds[0] > 0 && bounds[1] > 0) {
            validationCheck = value >= bounds[0] && value <= bounds[1]
        } else {
            if (bounds[0] > 0) {
                validationCheck = value >= bounds[0]
            }
            if (bounds[1] > 0) {
                validationCheck = value <= bounds[1]
            }
        }
        this._statisticsRecorder.record(name, validationCheck)

        return validationCheck
    }

    /**
     * @param {string} name
     * @param {number} lowerBound
     * @param {number} upperBound
     * @param getValueCallback
     * @return {boolean}
     */
    validateRangeFilter (name, lowerBound, upperBound, getValueCallback)
    {
        if (lowerBound > 0 || upperBound > 0) {
            return this.validateRange(name, getValueCallback(), [lowerBound, upperBound])
        }
        return true
    }

    /**
     * @param {string} text
     * @param {Object} rules
     * @param {string} key
     * @return {boolean}
     */
    validateTextContains (text, rules, key)
    {
        let validationCheck = true
        if (rules) {
            this._statisticsRecorder.record(key, validationCheck = text.match(rules) !== null)
        }
        return validationCheck
    }

    /**
     * @param {string} text
     * @param {Object} rules
     * @param {string} key
     * @return {boolean}
     */
    validateTextDoesNotContain (text, rules, key)
    {
        let validationCheck = true
        if (rules) {
            this._statisticsRecorder.record(key, validationCheck = text.match(rules) === null)
        }
        return validationCheck
    }
}