SankakuTagFormatter

Converts tags to lowercase in the sidebar and autocomplete on Sankaku Complex

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name        SankakuTagFormatter
// @namespace   SankakuTagFormatter
// @description Converts tags to lowercase in the sidebar and autocomplete on Sankaku Complex
// @author      Dramorian
// @match       http*://chan.sankakucomplex.com/*posts/*
// @match       https://chan.sankakucomplex.com/en/?tags=*
// @match       http*://idol.sankakucomplex.com/*posts/*
// @match       http*://beta.sankakucomplex.com/*posts/*
// @icon        https://www.google.com/s2/favicons?sz=64&domain=sankakucomplex.com
// @run-at      document-end
// @version     1.0.0
// @license     MIT
// ==/UserScript==

(function() {
  'use strict';

  /**
   * Configuration and utility constants
   */
  const CONFIG = {
    DEBUG: false,
    SELECTORS: {
      SIDEBAR: '#tag-sidebar',
      SIDEBAR_LINKS: 'ul > li a',
      AUTOCOMPLETE: '#autocomplete',
      AUTOCOMPLETE_TAGS: 'a b, a span'
    },
    DEBOUNCE_MS: 50
  };

  /**
   * Logging utilities
   */
  const logger = {
    log: (...args) => CONFIG.DEBUG && console.log('[SankakuTagFormatter]', ...args),
    error: (...args) => CONFIG.DEBUG && console.error('[SankakuTagFormatter]', ...args)
  };

  /**
   * Creates a debounced version of a function
   * @param {Function} fn - Function to debounce
   * @param {number} delay - Delay in milliseconds
   * @returns {Function} Debounced function
   */
  function debounce(fn, delay) {
    let timeoutId = null;
    return function(...args) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => fn.apply(this, args), delay);
    };
  }

  /**
   * Converts text content to lowercase for a single element
   * @param {HTMLElement} element - Element to convert
   */
  function lowercaseElement(element) {
    if (element?.textContent) {
      const originalText = element.textContent;
      const lowercasedText = originalText.toLowerCase();
      
      if (originalText !== lowercasedText) {
        element.textContent = lowercasedText;
      }
    }
  }

  /**
   * Converts text content to lowercase for text nodes
   * @param {Text} textNode - Text node to convert
   */
  function lowercaseTextNode(textNode) {
    const parent = textNode.parentNode;
    if (!parent || !textNode.nodeValue) return;
    
    // Only process if parent is a tag element within an anchor
    if ((parent.tagName === 'B' || parent.tagName === 'SPAN') && parent.closest('a')) {
      const originalValue = textNode.nodeValue;
      const lowercasedValue = originalValue.toLowerCase();
      
      if (originalValue !== lowercasedValue) {
        textNode.nodeValue = lowercasedValue;
      }
    }
  }

  /**
   * Sidebar tag converter
   */
  class SidebarConverter {
    convert() {
      const sidebar = document.querySelector(CONFIG.SELECTORS.SIDEBAR);
      
      if (!sidebar) {
        logger.error('Sidebar not found');
        return false;
      }

      const links = sidebar.querySelectorAll(CONFIG.SELECTORS.SIDEBAR_LINKS);
      logger.log(`Converting ${links.length} sidebar tags`);
      
      links.forEach(lowercaseElement);
      return true;
    }
  }

  /**
   * Autocomplete tag converter with observer
   */
  class AutocompleteConverter {
    #observer = null;
    #autocompleteElement = null;
    #processMutations = null;

    constructor() {
      // Create debounced mutation processor
      this.#processMutations = debounce(this.#handleMutations.bind(this), CONFIG.DEBOUNCE_MS);
    }

    /**
     * Processes mutations and converts tags to lowercase
     * @param {MutationRecord[]} mutations - Array of mutation records
     */
    #handleMutations(mutations) {
      for (const mutation of mutations) {
        if (mutation.type === 'childList') {
          // Handle added nodes
          for (const node of mutation.addedNodes) {
            if (node.nodeType === Node.ELEMENT_NODE) {
              const tags = node.querySelectorAll(CONFIG.SELECTORS.AUTOCOMPLETE_TAGS);
              tags.forEach(lowercaseElement);
            }
          }
        } else if (mutation.type === 'characterData') {
          // Handle text content changes
          lowercaseTextNode(mutation.target);
        }
      }
    }

    /**
     * Sets up observer for autocomplete element
     * @param {HTMLElement} element - Autocomplete element
     */
    #setupObserver(element) {
      this.#autocompleteElement = element;
      logger.log('Setting up autocomplete observer');

      // Convert any existing content first
      const existingTags = element.querySelectorAll(CONFIG.SELECTORS.AUTOCOMPLETE_TAGS);
      existingTags.forEach(lowercaseElement);

      // Create and start observer
      this.#observer = new MutationObserver(this.#processMutations);
      this.#observer.observe(element, {
        childList: true,
        subtree: true,
        characterData: true
      });

      logger.log('Autocomplete observer active');
    }

    /**
     * Waits for autocomplete element to appear in DOM
     * @returns {Promise<boolean>} Success status
     */
    async waitForAutocomplete() {
      return new Promise((resolve) => {
        // Check if already exists
        const existing = document.querySelector(CONFIG.SELECTORS.AUTOCOMPLETE);
        if (existing) {
          this.#setupObserver(existing);
          resolve(true);
          return;
        }

        // Set up observer to wait for it
        const bodyObserver = new MutationObserver((mutations) => {
          for (const mutation of mutations) {
            if (mutation.type === 'childList') {
              const autocomplete = document.querySelector(CONFIG.SELECTORS.AUTOCOMPLETE);
              if (autocomplete) {
                logger.log('Autocomplete element detected');
                this.#setupObserver(autocomplete);
                bodyObserver.disconnect();
                resolve(true);
                return;
              }
            }
          }
        });

        bodyObserver.observe(document.body, {
          childList: true,
          subtree: true
        });

        logger.log('Waiting for autocomplete element...');
      });
    }

    /**
     * Cleanup method to disconnect observer
     */
    destroy() {
      if (this.#observer) {
        this.#observer.disconnect();
        this.#observer = null;
        logger.log('Autocomplete observer disconnected');
      }
    }
  }

  /**
   * Main application controller
   */
  class SankakuTagFormatter {
    #sidebarConverter = null;
    #autocompleteConverter = null;

    async init() {
      logger.log('Initializing SankakuTagFormatter v2.0.0');

      try {
        // Convert sidebar tags
        this.#sidebarConverter = new SidebarConverter();
        const sidebarSuccess = this.#sidebarConverter.convert();
        
        if (sidebarSuccess) {
          logger.log('Sidebar conversion completed');
        }

        // Set up autocomplete monitoring
        this.#autocompleteConverter = new AutocompleteConverter();
        await this.#autocompleteConverter.waitForAutocomplete();

        logger.log('Initialization complete');
      } catch (error) {
        logger.error('Initialization error:', error);
        console.error('[SankakuTagFormatter] Fatal error:', error);
      }
    }

    destroy() {
      this.#autocompleteConverter?.destroy();
      logger.log('Formatter destroyed');
    }
  }

  /**
   * Bootstrap the script
   */
  function bootstrap() {
    const formatter = new SankakuTagFormatter();
    
    formatter.init().catch((error) => {
      console.error('[SankakuTagFormatter] Startup failed:', error);
    });

    // Cleanup on page unload (good practice)
    window.addEventListener('beforeunload', () => formatter.destroy());
  }

  // Start when DOM is ready
  if (['complete', 'loaded', 'interactive'].includes(document.readyState)) {
    bootstrap();
  } else {
    document.addEventListener('DOMContentLoaded', bootstrap);
  }

})();