Greasy Fork

Base Brazen Resource

Base library for my scripts

目前为 2020-11-17 提交的版本。查看 最新版本

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

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

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 LocalStore
{
    /**
     * @callback storeEventHandler
     * @param {Object} store
     */

    /**
     * @param {string} scriptPrefix
     * @param {Object} defaults
     * @return {LocalStore}
     */
    static createGlobalConfigStore (scriptPrefix, defaults)
    {
        return new LocalStore(scriptPrefix + 'globals', defaults)
    }

    static createPresetConfigStore (scriptPrefix, defaults)
    {
        return new LocalStore(scriptPrefix + 'presets', [
            {
                name: 'default',
                config: defaults,
            },
        ])
    }

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

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

        /**
         * @type {string}
         * @private
         */
        this._defaults = this._toJSON(defaults)

        /**
         * @type {storeEventHandler}
         */
        this._onChange = 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._defaults)
    }

    /**
     * @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])) {
                    arrayToObject = {}
                    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)
    }

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

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

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

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

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

    importFromFile (file)
    {
    }

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

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

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

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

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

    /**
     * @param {*} data
     * @return {LocalStore}
     */
    update (data)
    {
        this._store = data
        return this.save()
    }
}

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')
    }

    /**
     * @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().replace(' ', '-')
    }
}

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 {HTMLElement} textNode
     * @param {Object} rules
     * @return {Validator}
     */
    sanitizeTextNode (textNode, rules)
    {
        textNode.textContent = this.sanitize(textNode.textContent, rules)
        return this
    }

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

    /**
     * @param {string} name
     * @param {Node|HTMLElement} item
     * @param {string} selector
     * @return {boolean}
     */
    validateNodeExistence (name, item, selector)
    {
        let validationCheck = item.querySelector(selector) !== null
        this._statisticsRecorder.record(name, validationCheck)

        return validationCheck
    }

    /**
     * @param {string} name
     * @param {Node|HTMLElement} item
     * @param {string} selector
     * @return {boolean}
     */
    validateNodeNonExistence (name, item, selector)
    {
        let validationCheck = item.querySelector(selector) === null
        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
    }
}