Tokenize, merge, and subtract space-separated booru tag queries
Bu script direkt olarak kurulamaz. Başka scriptler için bir kütüphanedir ve meta yönergeleri içerir // @require https://update.greasyfork.org/scripts/583965/1880215/Brazen%20Framework%20-%20Tag%20Query%20Engine.js
// ==UserScript==
// @name Brazen Framework - Tag Query Engine
// @namespace brazenvoid
// @version 1.1.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Tokenize, merge, and subtract space-separated booru tag queries; includes Gelbooru-family search adapter (more adapters may be added later)
// @run-at document-end
// ==/UserScript==
/**
* Core tag-query engine: parenthesis-aware tokenize, idempotent merge, subtract for display.
* Site-specific URL/param handling lives in adapters (e.g. GelbooruFamilySearchAdapter) in this same module.
*/
class BrazenTagQueryEngine
{
// -------------------------------------------------------------------------
// Static public methods
// -------------------------------------------------------------------------
/**
* @param {string} token
* @return {string|null}
*/
static defaultMetatagKey(token)
{
let subject = token.startsWith('-') ? token.slice(1) : token
if (subject.startsWith('(')) {
return null
}
let colon = subject.indexOf(':')
return colon > 0 ? subject.slice(0, colon) : null
}
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
/**
* @param {{singleInstanceKeys?: Set<string>, metatagKey?: function(string): (string|null)}} [options]
*/
constructor(options = {})
{
this._singleInstanceKeys = options.singleInstanceKeys ?? new Set()
this._metatagKey = options.metatagKey ?? BrazenTagQueryEngine.defaultMetatagKey
}
// -------------------------------------------------------------------------
// Public class methods
// -------------------------------------------------------------------------
/**
* @param {string} query
* @return {string[]}
*/
tokenize(query)
{
let tokens = []
if (!query) {
return tokens
}
let index = 0
let length = query.length
while (index < length) {
while (index < length && /\s/.test(query[index])) {
index++
}
if (index >= length) {
break
}
let start = index
if (query[index] === '(') {
let depth = 0
while (index < length) {
if (query[index] === '(') {
depth++
} else if (query[index] === ')') {
depth--
if (depth === 0) {
index++
break
}
}
index++
}
} else {
while (index < length && !/\s/.test(query[index])) {
index++
}
}
tokens.push(query.slice(start, index))
}
return tokens
}
/**
* @param {string} token
* @return {string|null}
*/
metatagKey(token)
{
return this._metatagKey(token)
}
/**
* @param {string} userQuery
* @param {string[]} defaultTokens
* @return {string[]}
*/
merge(userQuery, defaultTokens)
{
let userTokens = this.tokenize(userQuery)
let userKeys = new Set()
let present = new Set(userTokens)
for (let token of userTokens) {
let key = this.metatagKey(token)
if (key) {
userKeys.add(key)
}
}
let merged = [...userTokens]
for (let token of defaultTokens) {
let key = this.metatagKey(token)
if (key && this._singleInstanceKeys.has(key) && userKeys.has(key)) {
continue
}
if (present.has(token)) {
continue
}
merged.push(token)
present.add(token)
}
return merged
}
/**
* @param {string[]} tokens
* @param {string[]} defaultTokens
* @return {string[]}
*/
subtract(tokens, defaultTokens)
{
let defaults = new Set(defaultTokens)
return tokens.filter((token) => !defaults.has(token))
}
}
/** Metatags Gelbooru cannot combine more than once per query. */
const GELBOORU_SINGLE_INSTANCE_METATAGS = new Set([
'sort', 'rating', 'user', 'md5', 'parent',
'aspectratio', 'aspectratiof', 'sourcedomains', 'width', 'height',
])
/**
* @typedef {object} GelbooruFamilySearchAdapterOptions
* @property {string} [queryParam]
* @property {string[]} [emptySentinels]
* @property {string} [listPath]
* @property {Set<string>} [singleInstanceKeys]
*/
/**
* Gelbooru 0.2 HTML search adapter (rule34.xxx, gelbooru.com list pages).
* Per-host URL variants (e.g. rule34.us `r=posts/index&q=`) override `listPath` / `queryParam`.
* Future site-family drivers stay as separate classes in this module.
*/
class GelbooruFamilySearchAdapter
{
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
/**
* @param {GelbooruFamilySearchAdapterOptions} [options]
*/
constructor(options = {})
{
this.queryParam = options.queryParam ?? 'tags'
this.emptySentinels = options.emptySentinels ?? ['all']
this.listPath = options.listPath ?? '/index.php?page=post&s=list'
this.singleInstanceKeys = options.singleInstanceKeys ?? GELBOORU_SINGLE_INSTANCE_METATAGS
this.engine = new BrazenTagQueryEngine({singleInstanceKeys: this.singleInstanceKeys})
}
// -------------------------------------------------------------------------
// Public class methods
// -------------------------------------------------------------------------
/**
* @param {Location|string} [locationRef]
* @return {string}
*/
readQuery(locationRef = location)
{
let params = new URLSearchParams(typeof locationRef === 'string' ? new URL(locationRef).search : locationRef.search)
return this.normalizeQueryParam(params.get(this.queryParam) ?? '')
}
/**
* Treats empty sentinels (e.g. `tags=all`) as an empty user query for default merging.
*
* @param {string|null} tags
* @return {string}
*/
normalizeQueryParam(tags)
{
if (!tags) {
return ''
}
let joined = this.engine.tokenize(tags).join(' ')
if (this.emptySentinels.includes(joined)) {
return ''
}
return tags
}
/**
* @param {string[]} tokens
* @return {string}
*/
serializeQuery(tokens)
{
return tokens.join(' ')
}
/**
* @param {string} serialized
* @param {string} [origin]
* @return {string}
*/
buildListUrl(serialized, origin = location.origin)
{
return origin + this.listPath + '&' + this.queryParam + '=' + encodeURIComponent(serialized)
}
/**
* Lowercase tag atoms; preserve metatag keys and OR-group syntax.
*
* @param {string} tag
* @return {string}
*/
normalizeAtom(tag)
{
tag = tag.trim()
if (!tag) {
return tag
}
let negated = false
if (tag.startsWith('-')) {
negated = true
tag = tag.slice(1).trimStart()
}
if (tag.startsWith('(') && tag.endsWith(')')) {
let inner = tag.slice(1, -1)
let parts = inner.split('~').map((part) => this.normalizeAtom(part.trim()))
tag = '(' + parts.join(' ~ ') + ')'
return negated ? '-' + tag : tag
}
let colonIdx = tag.indexOf(':')
if (colonIdx > 0) {
let key = tag.slice(0, colonIdx)
if (this.singleInstanceKeys.has(key)) {
return negated ? '-' + tag : tag
}
tag = key + ':' + tag.slice(colonIdx + 1).trim().replace(/\s+/g, '_').toLowerCase()
return negated ? '-' + tag : tag
}
tag = tag.replace(/\s+/g, '_').toLowerCase()
return negated ? '-' + tag : tag
}
}