BooruKit Core

Shared runtime and feature registry for the BooruKit userscript suite.

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.sleazyfork.org/scripts/589427/1890613/BooruKit%20Core.js

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         BooruKit Core
// @namespace    codeberg.org/0xdev/boorukit
// @version      0.1.0
// @description  Shared runtime and feature registry for the BooruKit userscript suite.
// @author       0xdev
// @license      LGPL-3.0-or-later
// ==/UserScript==

// THIS FILE IS AUTO-GENERATED — DO NOT MANUALLY EDIT.
// Source: boorukit/boorukit-core.library.js
(() => {
    'use strict';
    const BOORUKIT_API_VERSION = 1;
    const BOORUKIT_RUNTIME_KEY = Symbol.for('codeberg.org/0xdev/boorukit/runtime/v1');
    function defaultSettings() {
        return {
            schemaVersion: 1,
            modules: { grabber: true, 'pool-party': true },
            grabber: null,
            migrations: { bbdSettings: false },
        };
    }
    function validateSettings(value) {
        if (!value || typeof value !== 'object')
            return defaultSettings();
        const raw = value;
        const modules = raw.modules;
        const migrations = raw.migrations;
        const grabber = raw.grabber;
        if (raw.schemaVersion !== 1
            || !modules
            || typeof modules.grabber !== 'boolean'
            || typeof modules['pool-party'] !== 'boolean'
            || !migrations
            || typeof migrations.bbdSettings !== 'boolean'
            || (grabber !== null
                && (!grabber
                    || typeof grabber.writeTagsTxt !== 'boolean'))) {
            return defaultSettings();
        }
        return {
            schemaVersion: 1,
            modules: {
                grabber: modules.grabber,
                'pool-party': modules['pool-party'],
            },
            grabber: grabber === null
                ? null
                : { writeTagsTxt: grabber.writeTagsTxt },
            migrations: { bbdSettings: migrations.bbdSettings },
        };
    }
    function createStorageApi() {
        const GM = globalThis.GM;
        return {
            async read(key, codec) {
                const raw = await GM.getValue(key);
                return raw === undefined ? null : codec.decode(raw);
            },
            async write(key, value, codec) {
                await GM.setValue(key, codec.encode(value));
            },
            async delete(key) {
                await GM.deleteValue(key);
            },
        };
    }
    function sanitizeDiagnostic(error) {
        const sensitiveKey = /key|secret|token|password|credential|authorization|cookie|headers?|body|settings/i;
        const seen = new WeakSet();
        function sanitize(value, key) {
            if (key && sensitiveKey.test(key))
                return '[redacted]';
            if (typeof value === 'string') {
                return value
                    .replace(/https?:\/\/[^\s/@:]+:[^\s/@]+@[^\s]+/gi, '[redacted-url]')
                    .replace(/\b((?:proxy-)?authorization)\s*:\s*[^\r\n]+/gi, '$1: [redacted]')
                    .replace(/\b(set-cookie|cookie)\s*:\s*[^\r\n]+/gi, '$1: [redacted]')
                    .replace(/\bbearer\s+[^\s,;]+/gi, 'Bearer [redacted]')
                    .replace(/([?&](?:api[_-]?key|access[_-]?token|auth(?:orization)?|secret|password|credential|session)=)[^&#\s]+/gi, '$1[redacted]')
                    .replace(/\b(api[_-]?key|secret|token|password|credential)\s*[=:]\s*[^\s&,;]+/gi, '$1=[redacted]');
            }
            if (!value || typeof value !== 'object')
                return value;
            if (seen.has(value))
                return '[circular]';
            seen.add(value);
            if (value instanceof Error) {
                return {
                    name: value.name,
                    message: sanitize(value.message),
                };
            }
            if (Array.isArray(value)) {
                return value.map((item) => sanitize(item));
            }
            const sanitized = {};
            for (const [entryKey, entryValue] of Object.entries(value)) {
                sanitized[entryKey] = sanitize(entryValue, entryKey);
            }
            return sanitized;
        }
        try {
            return JSON.stringify(sanitize(error));
        }
        catch {
            return JSON.stringify({ name: 'Error', message: 'Diagnostic unavailable' });
        }
    }
    function createTaskCoordinator() {
        let cancelled = false;
        let activeLabel = null;
        const listeners = new Set();
        function publish(progress) {
            for (const listener of listeners)
                listener(progress);
        }
        const coordinator = {
            async run(label, operation) {
                if (activeLabel !== null) {
                    throw new Error('foreground task already running: ' + activeLabel);
                }
                activeLabel = label;
                cancelled = false;
                const signal = Object.freeze({
                    get cancelled() {
                        return cancelled;
                    },
                    throwIfCancelled() {
                        if (cancelled)
                            throw new Error('foreground task cancelled: ' + label);
                    },
                });
                publish({ label, completed: 0, total: 0 });
                try {
                    return await operation(signal, (completed, total) => {
                        publish({ label, completed, total });
                    });
                }
                finally {
                    activeLabel = null;
                    publish(null);
                }
            },
            cancel() {
                if (activeLabel !== null)
                    cancelled = true;
            },
            subscribe(listener) {
                listeners.add(listener);
                return () => { listeners.delete(listener); };
            },
        };
        return Object.freeze(coordinator);
    }
    function createSequentialQueue() {
        const queue = {
            async run(items, worker, policy) {
                const results = [];
                for (let index = 0; index < items.length; index += 1) {
                    if (policy.signal.cancelled)
                        break;
                    if (index > 0) {
                        if (policy.signal.cancelled)
                            break;
                        await policy.sleep(policy.throttleMs);
                        if (policy.signal.cancelled)
                            break;
                    }
                    if (policy.signal.cancelled)
                        break;
                    const item = items[index];
                    for (let attempt = 1; attempt <= policy.attempts; attempt += 1) {
                        if (policy.signal.cancelled)
                            break;
                        policy.onProgress?.(item, 'running', results.length, items.length);
                        if (policy.signal.cancelled)
                            break;
                        try {
                            const value = await worker(item, policy.signal);
                            results.push({ item, status: 'fulfilled', value });
                            policy.onProgress?.(item, 'done', results.length, items.length);
                            break;
                        }
                        catch (caught) {
                            const error = caught instanceof Error
                                ? caught
                                : new Error(String(caught));
                            if (policy.signal.cancelled)
                                break;
                            if (attempt < policy.attempts) {
                                policy.onProgress?.(item, 'retrying', results.length, items.length, error);
                                if (policy.signal.cancelled)
                                    break;
                                await policy.sleep(policy.retryDelayMs);
                                if (policy.signal.cancelled)
                                    break;
                                continue;
                            }
                            results.push({ item, status: 'rejected', reason: error });
                            policy.onProgress?.(item, 'failed', results.length, items.length, error);
                        }
                    }
                }
                return results;
            },
        };
        return Object.freeze(queue);
    }
    function createPageContext(href) {
        const url = new URL(href);
        const hostname = url.hostname.toLowerCase();
        const normalizedHostname = hostname.replace(/^www\./, '');
        const e621Hosts = new Set(['e621.net', 'e926.net']);
        const gelbooruHosts = new Set([
            'gelbooru.com',
            'hypnohub.net',
            'rule34.xxx',
            'safebooru.org',
        ]);
        const family = e621Hosts.has(normalizedHostname)
            ? 'e621'
            : gelbooruHosts.has(normalizedHostname)
                || normalizedHostname.endsWith('.booru.org')
                ? 'gelbooru'
                : 'unknown';
        let pageKind = 'other';
        if (family === 'e621') {
            if (url.pathname === '/posts' || url.pathname === '/posts/') {
                pageKind = 'gallery';
            }
            else if (/^\/posts\/\d+\/?$/.test(url.pathname)) {
                pageKind = 'post';
            }
            else if (/^\/pools\/\d+\/?$/.test(url.pathname)) {
                pageKind = 'pool';
            }
        }
        else if (family === 'gelbooru') {
            const page = url.searchParams.get('page');
            const section = url.searchParams.get('s');
            if (page === 'post' && section === 'list') {
                pageKind = 'gallery';
            }
            else if (page === 'post' && section === 'view') {
                pageKind = 'post';
            }
            else if (page === 'pool' && section === 'show') {
                pageKind = 'pool';
            }
        }
        return Object.freeze({
            url,
            origin: url.origin,
            hostname,
            normalizedHostname,
            family,
            pageKind,
        });
    }
    function createCoreRuntime(effects) {
        const descriptors = new Map();
        const tasks = createTaskCoordinator();
        const queue = createSequentialQueue();
        const diagnostics = Object.freeze({
            report(source, error) {
                effects.diagnosticSink(source, sanitizeDiagnostic(error));
            },
        });
        const settingsCodec = Object.freeze({
            decode: validateSettings,
            encode: validateSettings,
        });
        let settingsSnapshot = defaultSettings();
        const settings = Object.freeze({
            snapshot() {
                return validateSettings(settingsSnapshot);
            },
            async update(transform) {
                const current = validateSettings(settingsSnapshot);
                const next = validateSettings(transform(current));
                await effects.storage.write('boorukit.settings', next, settingsCodec);
                settingsSnapshot = next;
                return validateSettings(settingsSnapshot);
            },
        });
        let startPromise = null;
        async function activate() {
            const candidate = descriptors.get('deck');
            if (!candidate || candidate.kind !== 'deck') {
                diagnostics.report('core', new Error('required Deck module is not registered'));
                return;
            }
            if (!candidate.supports(effects.page))
                return;
            const stored = await effects.storage.read('boorukit.settings', settingsCodec);
            settingsSnapshot = stored === null
                ? defaultSettings()
                : validateSettings(stored);
            const deck = await candidate.activate({
                page: effects.page,
                tasks,
                diagnostics,
                settings,
            });
            const moduleLabels = {
                grabber: 'Grabber',
                'pool-party': 'Pool Party',
            };
            for (const id of ['grabber', 'pool-party']) {
                deck.registerSetting({
                    id,
                    label: moduleLabels[id],
                    checked: settingsSnapshot.modules[id],
                    reloadRequired: true,
                    onChange: async (checked) => {
                        await settings.update((current) => ({
                            ...current,
                            modules: { ...current.modules, [id]: checked },
                        }));
                    },
                });
            }
            for (const descriptor of descriptors.values()) {
                if (descriptor.kind !== 'feature')
                    continue;
                if (!settingsSnapshot.modules[descriptor.id])
                    continue;
                try {
                    if (!descriptor.supports(effects.page))
                        continue;
                    await descriptor.activate({
                        page: effects.page,
                        tasks,
                        diagnostics,
                        settings,
                        deck,
                        storage: effects.storage,
                        queue,
                    });
                }
                catch (error) {
                    const reason = sanitizeDiagnostic(error);
                    effects.diagnosticSink(descriptor.id, reason);
                    deck.setFeatureStatus(descriptor.id, 'unavailable', reason);
                }
            }
        }
        const api = Object.freeze({
            apiVersion: BOORUKIT_API_VERSION,
            register(descriptor) {
                if (descriptor.apiVersion !== BOORUKIT_API_VERSION) {
                    throw new Error(descriptor.id + ' requires API ' + String(descriptor.apiVersion)
                        + '; Core provides API 1');
                }
                if (descriptors.has(descriptor.id)) {
                    throw new Error('duplicate module id: ' + descriptor.id);
                }
                descriptors.set(descriptor.id, descriptor);
            },
            start() {
                startPromise ??= activate();
                return startPromise;
            },
        });
        return { api, descriptors };
    }
    function createRuntimeHarness() {
        const descriptors = new Map();
        const api = Object.freeze({
            apiVersion: BOORUKIT_API_VERSION,
            register(descriptor) {
                if (descriptor.apiVersion !== BOORUKIT_API_VERSION) {
                    throw new Error(descriptor.id + ' requires API ' + String(descriptor.apiVersion)
                        + '; Core provides API 1');
                }
                if (descriptors.has(descriptor.id)) {
                    throw new Error('duplicate module id: ' + descriptor.id);
                }
                descriptors.set(descriptor.id, descriptor);
            },
            start: async () => {
                throw new Error('Core runtime is not installed');
            },
        });
        return { api, descriptors };
    }
    function isRuntimeApi(value) {
        if (!value || typeof value !== 'object')
            return false;
        const candidate = value;
        return candidate.apiVersion === BOORUKIT_API_VERSION
            && typeof candidate.register === 'function'
            && typeof candidate.start === 'function';
    }
    if (typeof window !== 'undefined') {
        const runtimes = globalThis;
        const existing = runtimes[BOORUKIT_RUNTIME_KEY];
        if (existing !== undefined && !isRuntimeApi(existing)) {
            throw new Error('BooruKit runtime key contains an incompatible API');
        }
        if (existing === undefined) {
            const runtime = createCoreRuntime({
                page: createPageContext(location.href),
                storage: createStorageApi(),
                diagnosticSink(source, diagnostic) {
                    console.error('[BooruKit:' + source + '] ' + diagnostic);
                },
            });
            runtimes[BOORUKIT_RUNTIME_KEY] = runtime.api;
        }
    }
    if (typeof window === 'undefined'
        && typeof module !== 'undefined'
        && module.exports) {
        module.exports = {
            createCoreRuntime,
            createPageContext,
            createRuntimeHarness,
            createSequentialQueue,
            createStorageApi,
            createTaskCoordinator,
            defaultSettings,
            sanitizeDiagnostic,
            validateSettings,
        };
    }
})();