Greasy Fork is available in English.
Base library for my scripts
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/375557/880823/Base%20Brazen%20Resource.js
// ==UserScript==
// @name Base Brazen Resource
// @namespace brazenvoid
// @version 3.4.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Base library for my scripts
// @run-at document-end
// ==/UserScript==
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 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 = [], defaultValue = null)
{
return handler ? handler(...parameters) : defaultValue
}
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)
{
if (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
}
}