SankakuTagFormatter

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
  }

})();