Base library for my scripts
Version vom
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/375557/877455/Base%20Brazen%20Resource.js
// ==UserScript==
// @name Base Brazen Resource
// @namespace brazen
// @version 3.1.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Base library for my scripts
// @run-at document-end
// ==/UserScript==
const LINE_BREAK_REGEX = /\r?\n/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 'flag':
inputGroup = this._uiGen.createFormInputGroup(field.title, 'checkbox', field.helpText)
field.element = inputGroup.find('input')
break
case '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 'range':
inputGroup = this._uiGen.createFormRangeInputGroup(field.title, 'number', field.minimum, field.maximum, field.helpText)
field.element = inputGroup.find('input')
break
case 'ruleset':
inputGroup = this._uiGen.createFormTextAreaGroup(field.title, 2, field.helpText)
field.element = inputGroup.find('textarea')
break
case 'select':
inputGroup = this._uiGen.createDropDownFormGroup(field.title, field.options, field.helpText)
field.element = inputGroup.find('select')
break
}
return inputGroup
}
initialize (scriptPrefix)
{
this._localStore = new LocalStore(scriptPrefix + 'settings', this._toStoreObject())
this._localStore.onChange(() => this.updateInterface())
return this._syncLocalStore(true)
}
addFlagField (name, helpText)
{
this._createField('flag', name, false, helpText)
return this
}
addNumberField (name, minimum, maximum, helpText)
{
let field = this._createField('number', name, minimum, helpText)
field.minimum = minimum
field.maximum = maximum
return this
}
addRangeField (name, minimum, maximum, helpText)
{
let field = this._createField('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('ruleset', name, [], helpText)
field.onEncode = onEncode ?? field.onEncode
field.onDecode = onDecode ?? field.onDecode
return this
}
addSelectField (name, options, helpText)
{
let field = this._createField('select', name, '', helpText)
field.options = options
return this
}
addTextField (name, helpText)
{
this._createField('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 'flag':
field.value = field.element.prop('checked')
break
case 'number':
field.value = parseInt(field.element.val())
break
case 'range':
field.value = {
minimum: field.element.first().val(),
maximum: field.element.last().val(),
}
break
case 'ruleset':
value = Utilities.trimAndKeepNonEmptyStrings(field.element.val().split(LINE_BREAK_REGEX))
field.value = field.onDecode ? field.onDecode(value, field) : value
break
default:
field.value = field.element.val()
}
}
}
return this
}
updateInterface ()
{
let field, value
for (let fieldName in this._config) {
field = this._config[fieldName]
if (field.element) {
switch (field.type) {
case 'flag':
field.element.prop('checked', field.value)
break
case 'number':
field.element.val(field.value)
break
case 'range':
field.element.first().val(field.value.minimum)
field.element.last().val(field.value.maximum)
break
case '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 {string}
* @private
*/
this._defaults = Utilities.objectToJSON(defaults)
/**
* @type {boolean}
* @private
*/
this._defaultsSet = false
/**
* @type {string}
* @private
*/
this._key = key
// Events
/**
* @type {storeEventHandler}
*/
this._onChange = null
}
/**
* @return {Object}
* @private
*/
_getDefaults ()
{
return Utilities.objectFromJSON(this._defaults)
}
_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 {LocalStore}
*/
restoreDefaults ()
{
this._defaultsSet = true
this._handleOnChange()
return this._getDefaults()
}
/**
* @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 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 {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
}
}