Greasy Fork

Base Resource

Base library for my scripts

目前为 2019-12-16 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.icu/scripts/375557/758353/Base%20Resource.js

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

GM_addStyle(`
    button.form-button { 
        padding: 0 5px;
        width: 100%; 
    }
    button.show-settings { 
        background-color: #ffa31a; 
        border: 0; 
        margin: 2px 5px; 
        padding: 2px 5px; 
        width: 100%; 
    }
    button.show-settings.fixed { 
        color: black; 
        font-size: 14px; 
        left: 0; 
        margin: 0; 
        padding: 15px 0px; 
        position: fixed; 
        top: 250px; 
        width: 30px; 
        writing-mode: sideways-lr; 
        z-index: 999; 
    }
    
    div.form-actions { 
        text-align: center; 
    }
    div.form-actions button + button { 
        margin-left: 10px; 
    }
    div.form-actions button.form-button { 
        padding: 0 15px;
        width: auto
    }
    div.form-actions-wrapper { 
        display: inline-flex;
    }
    div.form-actions-wrapper > div.form-group + * {
        margin-left: 15px;
    }
    div.form-group { 
        min-height: 15px;
        padding: 5px 0; 
    }
    div.form-group.form-range-input-group > input {
        padding: 0 5px;
        width: 70px; 
    }
    div.form-group.form-range-input-group > input + input {
        margin-right: 5px;
    }
    div.form-section { 
        text-align: center; 
    }
    div.form-section button + button { 
        margin-left: 5px; 
    }
    div.form-section label.title { 
        display: block; 
        height: 20px; 
        width: 100%; 
    }
    div.form-section button.form-button {
        width: auto; 
    }
    
    hr { 
        margin: 3px; 
    }
    
    input.form-input { 
        height: 18px; 
        text-align: center; 
    }
    input.form-input.check-radio-input { 
        float: left; 
        margin-right: 5px; 
    }
    input.form-input.regular-input { 
        float: right; 
        width: 100px; 
    }
    
    label.form-label { 
        padding: 2px 0; 
    }
    label.form-label.regular-input { 
        float: left; 
    }
    label.form-label.check-radio-input { 
        float: right; 
    }
    label.form-stat-label { 
        float: right; 
        padding: 2px 0; 
    }
    
    section.form-section { 
        color: black; 
        font-size: 12px; 
        font-weight: bold; 
        position: fixed; 
        left: 0; 
        padding: 5px 10px; 
        z-index: 1000; 
    }
    
    select.form-dropdown { 
        float: right; 
        height: 18px; 
        text-align: center; 
        width: 100px 
    }
`)

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

/**
 * @param {string} classes
 * @return {RegExp}
 * @private
 */
function _generateClassesIdentificationRegex (classes)
{
    return new RegExp('(\\s|^)' + classes + '(\\s|$)')
}

/**
 * @param {Element} node
 * @param {string} classes
 * @return {Element}
 */
function addClasses (node, classes)
{
    if (!hasClasses(node, classes)) {
        if (node.className !== '') {
            node.className += ' '
        }
        node.className += classes
    }
    return node
}

/**
 * @param {Element} node
 * @param {string} classes
 * @return {boolean}
 */
function hasClasses (node, classes)
{
    return !!node.className.match(_generateClassesIdentificationRegex(classes))
}

/**
 * @param {Element} node
 * @param {string} classes
 * @return {Element}
 */
function removeClasses (node, classes)
{
    if (hasClasses(node, classes)) {
        node.className = node.className.replace(_generateClassesIdentificationRegex(classes), ' ')
    }
    return node
}

class CaseConverters
{
    /**
     * @param {string} text
     * @return {string}
     */
    static toCamel (text)
    {
        return text.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
            return index === 0 ? letter.toLowerCase() : letter.toUpperCase()
        }).replace(/\s+/g, '')
    }

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

    /**
     * @param {string} text
     * @return {string}
     */
    static toKebabFromSnake (text)
    {
        return text.replace('_', '-')
    }

    /**
     * @param {string} text
     * @return {string}
     */
    static toNormalFromKebab (text)
    {
        return text.replace('-', ' ')
    }

    /**
     * @param {string} text
     * @return {string}
     */
    static toNormalFromSnake (text)
    {
        return text.replace('_', ' ')
    }

    /**
     * @param {string} text
     * @return {string}
     */
    static toSnake (text)
    {
        return text.toLowerCase().replace(' ', '_')
    }
}

class ChildObserver
{
    /**
     * @callback observerMutationHandler
     * @param {Node} target
     */

    /**
     * @param {Element|Element[]} nodes
     * @param {observerMutationHandler} handler
     * @param {boolean} doInitialRun
     * @return {ChildObserver}
     */
    static observe (nodes, handler, doInitialRun = false)
    {
        let instance = new ChildObserver(handler)
        instance.observeNodes(nodes, doInitialRun)
        return instance
    }

    /**
     * @param {observerMutationHandler} handler
     */
    constructor (handler)
    {
        /**
         * @type {{subtree: boolean, attributes: boolean, childList: boolean}}
         * @private
         */
        this._config = {
            attributes: false,
            childList: true,
            subtree: false,
        }

        /**
         * @type {observerMutationHandler}
         * @private
         */
        this._handler = handler

        /**
         * @type {MutationObserver}
         * @private
         */
        this._observer = new MutationObserver(function (mutations) {
            for (let mutation of mutations) {
                handler(mutation.target)
            }
        })
    }

    /**
     * @param {Element|Element[]} nodes
     * @param {boolean} doInitialRun
     */
    observeNodes (nodes, doInitialRun = false)
    {
        nodes = (Array.isArray(nodes) || nodes instanceof NodeList) ? nodes : [nodes]
        for (let node of nodes) {
            if (doInitialRun) {
                this._handler(node)
            }
            this._observer.observe(node, this._config)
        }
    }
}

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

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

        /**
         * @type {Object}
         * @private
         */
        this._store = {}

        /**
         * @type {string}
         * @private
         */
        this._defaultStore = this._toJSON(defaultStore)

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

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

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

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

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

            for (let key in arrayObject) {
                store[property].push(arrayObject[key])
            }
        }
        for (let property in parsedJSON.objects) {
            store[property] = this._fromJSON(parsedJSON.objects[property])
        }
        for (let property in parsedJSON.properties) {
            store[property] = parsedJSON.properties[property]
        }
        return store
    }

    /**
     * @return {string}
     * @private
     */
    _getStore ()
    {
        return window.localStorage.getItem(this._key)
    }

    /**
     * @return {Object}
     * @private
     */
    _getDefaults ()
    {
        return this._fromJSON(this._defaultStore)
    }

    /**
     * @param {Object} store
     * @return {string}
     * @private
     */
    _toJSON (store)
    {
        let arrayToObject = {}
        let json = {arrays: {}, objects: {}, properties: {}}

        for (let property in store) {
            if (typeof store[property] === 'object') {
                if (Array.isArray(store[property])) {
                    for (let key in store[property]) {
                        arrayToObject[key] = store[property][key]
                    }
                    json.arrays[property] = JSON.stringify(arrayToObject)
                } else {
                    json.objects[property] = this._toJSON(store[property])
                }
            } else {
                json.properties[property] = store[property]
            }
        }
        return JSON.stringify(json)
    }

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

    /**
     * @return {Object}
     */
    get ()
    {
        return this._store
    }

    /**
     * @return {LocalStore}
     */
    restoreDefaults ()
    {
        this._store = this._getDefaults()

        if (this.onDefaultsLoaded !== null) {
            this.onDefaultsLoaded(this._store)
        }
        return this
    }

    /**
     * @return {LocalStore}
     */
    retrieve ()
    {
        let storedStore = this._getStore()
        if (storedStore === null) {
            this.restoreDefaults()
        } else {
            this._store = this._fromJSON(storedStore)
        }
        if (this.onRetrieval !== null) {
            this.onRetrieval(this._store)
        }
        return this
    }

    /**
     * @return {LocalStore}
     */
    save ()
    {
        window.localStorage.setItem(this._key, this._toJSON(this._store))

        if (this.onUpdated !== null) {
            this.onUpdated(this._store)
        }
        return this
    }

    /**
     * @return {boolean}
     */
    isPurged ()
    {
        return this._getStore() === null
    }
}

class Logger
{
    /**
     * @param {boolean} enableDebugging
     */
    constructor (enableDebugging)
    {
        /**
         * @type {boolean}
         * @private
         */
        this._enableDebugging = enableDebugging
    }

    /**
     * @param {string} message
     * @private
     */
    _log (message)
    {
        if (this._enableDebugging) {
            console.log(message)
        }
    }

    /**
     * @param {string} task
     */
    logTaskCompletion (task)
    {
        this._log('Completed: ' + task)
        this.logSeparator()
    }

    logSeparator ()
    {
        this._log('------------------------------------------------------------------------------------')
    }

    /**
     * @param {string}  filterName
     * @param {boolean} validationResult
     */
    logValidation (filterName, validationResult)
    {
        this._log('Satisfies ' + filterName + ' Filter: ' + (validationResult ? 'true' : 'false'))
    }

    /**
     * @param {string} videoName
     */
    logVideoCheck (videoName)
    {
        this._log('Checking Video: ' + videoName)
    }
}

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(CaseConverters.toKebab(settingName) + '-setting')
    }

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

    /**
     * @param {string} statisticType
     * @return {string}
     */
    getStatLabelSelector (statisticType)
    {
        return this.getSelector(CaseConverters.toKebab(statisticType) + '-stat')
    };
}

class StatisticsRecorder
{
    /**
     * @param {Logger} logger
     * @param {SelectorGenerator} selectorGenerator
     */
    constructor (logger, selectorGenerator)
    {
        /**
         * @type {Logger}
         * @private
         */
        this._logger = logger

        /**
         * @type {SelectorGenerator}
         * @private
         */
        this._selectorGenerator = selectorGenerator

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

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

    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 UIGenerator
{
    /**
     * @param {Element|Node} node
     */
    static appendToBody (node)
    {
        document.getElementsByTagName('body')[0].appendChild(node)
    }

    /**
     * @param {Element} node
     * @param {Element[]} children
     * @return {Element}
     */
    static populateChildren (node, children)
    {
        for (let child of children) {
            node.appendChild(child)
        }
        return node
    }

    /**
     * @param {boolean} showUI
     * @param {SelectorGenerator} selectorGenerator
     */
    constructor (showUI, selectorGenerator)
    {
        /**
         * @type {*}
         * @private
         */
        this._buttonBackroundColor = null

        /**
         * @type {SelectorGenerator}
         * @private
         */
        this._selectorGenerator = selectorGenerator

        /**
         * @type {boolean}
         * @private
         */
        this._showUI = showUI
    }

    /**
     * @param {Element[]} children
     * @return {HTMLDivElement}
     */
    createFormActions (children)
    {
        let wrapperDiv = document.createElement('div')
        wrapperDiv.classList.add('form-actions-wrapper')

        UIGenerator.populateChildren(wrapperDiv, children)

        let formActionsDiv = document.createElement('div')
        formActionsDiv.classList.add('form-actions')
        formActionsDiv.appendChild(wrapperDiv)

        return formActionsDiv
    }

    /**
     * @param {string} caption
     * @param onClick
     * @return {HTMLButtonElement}
     */
    createFormButton (caption, onClick)
    {
        let button = document.createElement('button')
        button.classList.add('form-button')
        button.textContent = caption
        button.addEventListener('click', onClick)

        if (this._buttonBackroundColor !== null) {
            button.style.backgroundColor = this._buttonBackroundColor
        }
        return button
    }

    /**
     * @param {Element[]} children
     * @return {Element}
     */
    createFormGroup (children)
    {
        let divFormGroup = document.createElement('div')
        divFormGroup.classList.add('form-group')

        return UIGenerator.populateChildren(divFormGroup, children)
    }

    /**
     * @param {string} id
     * @param {Array} keyValuePairs
     * @param {*} defaultValue
     * @return {HTMLSelectElement}
     */
    createFormGroupDropdown (id, keyValuePairs, defaultValue = null)
    {
        let dropdown = document.createElement('select'), item
        dropdown.id = id
        dropdown.classList.add('form-dropdown')

        for (let [key, value] of keyValuePairs) {
            item = document.createElement('option')
            item.textContent = value
            item.value = key
            dropdown.appendChild(item)
        }
        dropdown.value = defaultValue === null ? keyValuePairs[0][0] : defaultValue

        return dropdown
    }

    /**
     * @param {string} id
     * @param {string} type
     * @param {*} defaultValue
     * @return {HTMLInputElement}
     */
    createFormGroupInput (id, type, defaultValue = null)
    {
        let inputFormGroup = document.createElement('input')
        inputFormGroup.id = id
        inputFormGroup.classList.add('form-input')
        inputFormGroup.type = type

        switch (type) {
            case 'number':
            case 'text':
                inputFormGroup.classList.add('regular-input')

                if (defaultValue !== null) {
                    inputFormGroup.value = defaultValue
                }
                break
            case 'radio':
            case 'checkbox':
                inputFormGroup.classList.add('check-radio-input')

                if (defaultValue !== null) {
                    inputFormGroup.checked = defaultValue
                }
                break
        }
        return inputFormGroup
    }

    /**
     * @param {string} label
     * @param {string} inputID
     * @param {string} inputType
     * @return {HTMLLabelElement}
     */
    createFormGroupLabel (label, inputID = '', inputType = '')
    {
        let labelFormGroup = document.createElement('label')
        labelFormGroup.classList.add('form-label')
        labelFormGroup.textContent = label

        if (inputID !== '') {
            labelFormGroup.setAttribute('for', inputID)
        }
        if (inputType !== '') {
            switch (inputType) {
                case 'number':
                case 'text':
                    labelFormGroup.classList.add('regular-input')
                    labelFormGroup.textContent += ': '
                    break
                case 'radio':
                case 'checkbox':
                    labelFormGroup.classList.add('check-radio-input')
                    break
            }
        }
        return labelFormGroup
    }

    /**
     * @param {string} statisticType
     * @return {HTMLLabelElement}
     */
    createFormGroupStatLabel (statisticType)
    {
        let labelFormGroup = document.createElement('label')
        labelFormGroup.id = this._selectorGenerator.getStatLabelSelector(statisticType)
        labelFormGroup.classList.add('form-stat-label')
        labelFormGroup.textContent = '0'

        return labelFormGroup
    }

    /**
     * @param {string} label
     * @param {string} inputType
     * @param {*} defaultValue
     * @return {Element}
     */
    createFormInputGroup (label, inputType = 'text', defaultValue = null)
    {
        let divFormInputGroup
        let inputID = this._selectorGenerator.getSettingsInputSelector(label)
        let labelFormGroup = this.createFormGroupLabel(label, inputType, inputID)
        let inputFormGroup = this.createFormGroupInput(inputID, inputType, defaultValue)

        switch (inputType) {
            case 'number':
            case 'text':
                divFormInputGroup = this.createFormGroup([labelFormGroup, inputFormGroup])
                break
            case 'radio':
            case 'checkbox':
                divFormInputGroup = this.createFormGroup([inputFormGroup, labelFormGroup])
                break
        }
        return divFormInputGroup
    }

    /**
     * @param {string} label
     * @param {string} inputsType
     * @param {*} defaultValues
     * @return {Element}
     */
    createFormRangeInputGroup (label, inputsType = 'text', defaultValues = null)
    {
        let divFormInputGroup = this.createFormGroup([
            this.createFormGroupLabel(label, null, inputsType),
            this.createFormGroupInput(
                this._selectorGenerator.getSettingsRangeInputSelector(label, false),
                inputsType,
                defaultValues === null ? null : defaultValues[1],
            ),
            this.createFormGroupInput(
                this._selectorGenerator.getSettingsRangeInputSelector(label, true),
                inputsType,
                defaultValues === null ? null : defaultValues[0],
            ),
        ])
        divFormInputGroup.classList.add('form-range-input-group')

        return divFormInputGroup
    }

    /**
     * @param {string} title
     * @param {Element[]} children
     * @return {Element}
     */
    createFormSection (title, children)
    {
        let sectionDiv = document.createElement('div')
        sectionDiv.classList.add('form-section')

        let sectionTitle = document.createElement('label')
        sectionTitle.textContent = title
        sectionTitle.classList.add('title')

        UIGenerator.populateChildren(sectionDiv, [sectionTitle])

        return UIGenerator.populateChildren(sectionDiv, children)
    }

    /**
     * @param {string} caption
     * @param {string} tooltip
     * @param onClick
     * @return {HTMLButtonElement}
     */
    createFormSectionButton (caption, tooltip, onClick)
    {
        let button = this.createFormButton(caption, onClick)
        button.title = tooltip

        return button
    }

    /**
     * @param {string} IDSuffix
     * @param {*} backgroundColor
     * @param {*} top
     * @param {*} width
     * @param {Element[]} children
     * @return {Element}
     */
    createSection (IDSuffix, backgroundColor, top, width, children)
    {
        let section = document.createElement('section')
        section.id = this._selectorGenerator.getSelector(IDSuffix)
        section.classList.add('form-section')
        section.style.display = this._showUI ? 'block' : 'none'
        section.style.top = top
        section.style.width = width
        section.style.backgroundColor = backgroundColor

        return UIGenerator.populateChildren(section, children)
    }

    /**
     * @return {HTMLHRElement}
     */
    createSeparator ()
    {
        return document.createElement('hr')
    }

    /**
     * @param {LocalStore} localStore
     * @param onClick
     * @param {boolean} addTopPadding
     * @return {HTMLDivElement}
     */
    createSettingsFormActions (localStore, onClick, addTopPadding = false)
    {
        let divFormActions = this.createFormActions([
            this.createFormButton('Apply', onClick),
            this.createFormButton('Reset', function () {
                localStore.retrieve()
                onClick()
            }),
        ])
        if (addTopPadding) {
            divFormActions.style.paddingTop = '10px'
        }
        return divFormActions
    }

    /**
     * @param {string} label
     * @param {Array} keyValuePairs
     * @param {*} defaultValue
     * @return {Element}
     */
    createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null)
    {
        let dropdownID = this._selectorGenerator.getSettingsInputSelector(label)

        return this.createFormGroup([
            this.createFormGroupLabel(label, 'text', dropdownID),
            this.createFormGroupDropdown(dropdownID, keyValuePairs, defaultValue),
        ])
    }

    /**
     * @param {string} settingsSectionIDSuffix
     * @return {HTMLButtonElement}
     */
    createSettingsHideButton (settingsSectionIDSuffix)
    {
        let settingsSectionID = this._selectorGenerator.getSelector(settingsSectionIDSuffix)
        return this.createFormButton('Hide', function () {
            document.getElementById(settingsSectionID).style.display = 'none'
        })
    }

    /**
     * @param {string} caption
     * @param {Element} settingsSection
     * @param {boolean} fixed
     * @return {HTMLButtonElement}
     */
    createSettingsShowButton (caption, settingsSection, fixed = true)
    {
        let controlButton = document.createElement('button')
        controlButton.textContent = caption
        controlButton.classList.add('show-settings')

        if (fixed) {
            controlButton.classList.add('fixed')
        }
        controlButton.addEventListener('click', function () {
            let settingsUI = document.getElementById(settingsSection.id)
            settingsUI.style.display = settingsUI.style.display === 'none' ? 'block' : 'none'
        })
        return controlButton
    }

    /**
     * @param {string} statisticsType
     * @param {string} label
     * @return {Element}
     */
    createStatisticsFormGroup (statisticsType, label = null)
    {
        if (label === null) {
            label = statisticsType
        }
        return this.createFormGroup([
            this.createFormGroupLabel('Filtered ' + label + ' Videos'),
            this.createFormGroupStatLabel(statisticsType),
        ])
    }

    /**
     * @param {LocalStore} localStore
     * @return {Element}
     */
    createStoreFormSection (localStore)
    {
        return this.createFormSection('Store', [
            this.createFormActions([
                this.createFormSectionButton('Update', 'Save UI settings in store', function () {
                    localStore.save()
                }),
                this.createFormSectionButton(
                    'Reset',
                    'Reset store values to user defaults',
                    function () {
                        localStore.restoreDefaults()
                    },
                ),
                this.createFormSectionButton('Purge', 'Purge store', function () {
                    localStore.delete()
                }),
            ]),
        ])
    }

    /**
     * @param {string} label
     * @return {HTMLElement}
     */
    getSettingsInput (label)
    {
        return document.getElementById(this._selectorGenerator.getSettingsInputSelector(label))
    }

    /**
     * @param {string} label
     * @return {boolean}
     */
    getSettingsInputCheckedStatus (label)
    {
        return this.getSettingsInput(label).checked
    }

    /**
     * @param {string} label
     * @param {boolean} lowerBound
     * @return {*}
     */
    getSettingsInputValue (label)
    {
        return this.getSettingsInput(label).value
    }

    /**
     * @param {string} label
     * @param {boolean} getMinInput
     * @return {HTMLElement}
     */
    getSettingsRangeInput (label, getMinInput)
    {
        return document.getElementById(this._selectorGenerator.getSettingsRangeInputSelector(label, getMinInput))
    }

    /**
     * @param {string} label
     * @param {boolean} getMinInputValue
     * @return {*}
     */
    getSettingsRangeInputValue (label, getMinInputValue)
    {
        return this.getSettingsRangeInput(label, getMinInputValue).value
    }

    /**
     * @param {string} label
     * @param {boolean} bool
     */
    setSettingsInputCheckedStatus (label, bool)
    {
        this.getSettingsInput(label).checked = bool
    }

    /**
     * @param {string} label
     * @param {*} value
     */
    setSettingsInputValue (label, value)
    {
        this.getSettingsInput(label).value = value
    }

    /**
     * @param {string} label
     * @param {number} lowerBound
     * @param {number} upperBound
     */
    setSettingsRangeInputValue (label, lowerBound, upperBound)
    {
        this.getSettingsRangeInput(label, true).value = lowerBound
        this.getSettingsRangeInput(label, false).value = upperBound
    }
}

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

    /**
     * @param {StatisticsRecorder} statisticsRecorder
     */
    constructor (statisticsRecorder)
    {
        /**
         * @type {Array}
         * @private
         */
        this._blacklist = []

        /**
         * @type {Array}
         * @private
         */
        this._filters = []

        /**
         * @type {RegExp}
         * @private
         */
        this._optimizedBlacklist = null

        /**
         * @type {Object}
         * @private
         */
        this._optimizedSanitizationRules = {}

        /**
         * @type {Object}
         * @private
         */
        this._sanitizationRules = []

        /**
         * @type {StatisticsRecorder}
         * @private
         */
        this._statisticsRecorder = statisticsRecorder
    }

    _buildWholeWordMatchingRegex (words)
    {
        let patternedWords = []
        for (let i = 0; i < words.length; i++) {
            patternedWords.push('\\b' + words[i] + '\\b')
        }
        return new RegExp('(' + patternedWords.join('|') + ')', 'gi')
    }

    /**
     * @param {string[]} blacklistedWords
     * @return {Validator}
     */
    addBlacklistFilter (blacklistedWords)
    {
        this._blacklist = blacklistedWords
        return this
    }

    /**
     * @param {Object} sanitizationRules
     * @return {Validator}
     */
    addSanitizationFilter (sanitizationRules)
    {
        this._sanitizationRules = sanitizationRules
        return this
    }

    /**
     * @return {Validator}
     */
    optimize ()
    {
        this._optimizedBlacklist = this._buildWholeWordMatchingRegex(this._blacklist)

        for (const substitute in this._sanitizationRules) {
            this._optimizedSanitizationRules[substitute] =
                this._buildWholeWordMatchingRegex(this._sanitizationRules[substitute])
        }
        return this
    }

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

    /**
     * @param {Element} videoNameNode
     * @return {Validator}
     */
    sanitizeVideoItem (videoNameNode)
    {
        videoNameNode.textContent = this.sanitize(videoNameNode.textContent)
        return this
    }

    /**
     * @param {string} videoNameNodeSelector
     * @return {Validator}
     */
    sanitizeVideoPage (videoNameNodeSelector)
    {
        let videoNameNode = document.querySelector(videoNameNodeSelector)
        if (videoNameNode !== null) {

            let sanitizedVideoName = this.sanitize(videoNameNode.textContent)
            videoNameNode.textContent = sanitizedVideoName
            document.title = sanitizedVideoName
        }
        return this
    }

    /**
     * @param {string} text
     * @return {boolean}
     */
    validateBlackList (text)
    {
        let validationCheck = true

        if (this._optimizedBlacklist !== null) {
            validationCheck = text.match(this._optimizedBlacklist) === null
            this._statisticsRecorder.record('Blacklist', 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
    }
}