// ==UserScript==
// @name Base Brazen Resource
// @namespace brazen
// @version 3.0.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Base library for my scripts
// @require https://code.jquery.com/jquery-3.5.1.min.js
// @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
}
}