Tokenize, merge, and subtract space-separated booru tag queries
This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/583965/1857992/Brazen%20Framework%20-%20Tag%20Query%20Engine.js
// ==UserScript==
// @name Brazen Framework - Tag Query Engine
// @namespace brazenvoid
// @version 1.0.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Tokenize, merge, and subtract space-separated booru tag queries
// @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).
*/
class BrazenTagQueryEngine
{
/**
* @param {{singleInstanceKeys?: Set<string>, metatagKey?: function(string): (string|null)}} [options]
*/
constructor(options = {})
{
this._singleInstanceKeys = options.singleInstanceKeys ?? new Set()
this._metatagKey = options.metatagKey ?? BrazenTagQueryEngine.defaultMetatagKey
}
/**
* @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
}
/**
* @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))
}
}