Greasy Fork is available in English.

Link Formatter & Counter

Форматирует ссылки и считает символы

// ==UserScript==
// @name         Link Formatter & Counter
// @icon         https://www.google.com/s2/favicons?domain=shikimori.me
// @namespace    https://shikimori.one
// @version      1.0
// @description  Форматирует ссылки и считает символы
// @author       LifeH
// @match        *://shikimori.org/*
// @match        *://shikimori.one/*
// @match        *://shikimori.me/*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  let formatterON = localStorage.getItem("formatterON") !== "false";
  let counterON = localStorage.getItem("counterON") !== "false";

  function settingsPage() {
    const currentURL = window.location.pathname;
    const accountSettingsPattern = /\/[a-zA-Z0-9_-]+\/edit\/account/;
    return accountSettingsPattern.test(currentURL);
  }

  function URLcheck(str) {
    let pattern = new RegExp(
      "^(http|ftp|https):\\/\\/([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])",
      "i"
    );
    return !!pattern.test(str.trim());
  }

  function formatter(clipboard, selectedText) {
    if (!formatterON) return;

    let formattedText = null;
    if (!URLcheck(clipboard)) {
      return;
    }

    const url = new URL(clipboard);
    const urlType = url.pathname.split("/");
    let id = urlType[2]?.split("-")[0];

    if (id && id.startsWith("z")) {
      id = id.substring(1);
    }

    selectedText = selectedText.trim();
    if (!selectedText) {
      return;
    }

    if (url.hostname.includes("shikimori") && urlType.length >= 3) {
      switch (urlType[1]) {
        case "characters":
          formattedText = `[character=${id}]${selectedText}[/character]`;
          break;
        case "ranobe":
          formattedText = `[ranobe=${id}]${selectedText}[/ranobe]`;
          break;
        case "mangas":
          formattedText = `[manga=${id}]${selectedText}[/manga]`;
          break;
        case "animes":
          formattedText = `[anime=${id}]${selectedText}[/anime]`;
          break;
        case "persons":
          formattedText = `[person=${id}]${selectedText}[/person]`;
          break;
        default:
          formattedText = `[url=${clipboard}]${selectedText}[/url]`;
          break;
      }
    } else {
      formattedText = `[url=${clipboard}]${selectedText || clipboard}[/url]`;
    }

    return formattedText;
  }

  function vstavka(event, textarea) {
    if (!formatterON) return;

    let clipboardData = (event.originalEvent || event).clipboardData;
    if (!clipboardData) {
      return;
    }

    let clipboard = clipboardData.getData("text");

    if (!URLcheck(clipboard)) {
      return;
    }

    let selectedText = textarea[0].value.substring(
      textarea[0].selectionStart,
      textarea[0].selectionEnd
    );

    if (!selectedText.trim()) {
      return;
    }

    let formattedText = formatter(clipboard, selectedText);

    if (formattedText) {
      let text = textarea.val();
      let start = textarea[0].selectionStart;
      let end = textarea[0].selectionEnd;

      let newText = text.slice(0, start) + formattedText + text.slice(end);
      textarea.val(newText);

      textarea[0].setSelectionRange(
        start + formattedText.length,
        start + formattedText.length
      );

      event.preventDefault();
    }
  }

  function addCounter(textarea) {
    if (!counterON) return;

    if (textarea.next("#Counter").length === 0) {
      let counter = $(
        '<div id="Counter" style="position: absolute; right: 5px; bottom: 15px; font-size: 12px; color: gray;"></div>'
      );
      textarea.after(counter);

      textarea.on("input", function () {
        let text = textarea.val().replace(/(\r\n|\n|\r)/g, "  ");
        let charCount = text.length;
        let wordCount = textarea[0].value
          .trim()
          .split(/\s+/)
          .filter(Boolean).length;

        counter.text(`Символов: ${charCount} | Слов: ${wordCount}`);
      });

      textarea.trigger("input");
    }
  }

  function formatterConf(state) {
    formatterON = state;
    localStorage.setItem("formatterON", state);
  }

  function counterConf(state) {
    counterON = state;
    localStorage.setItem("counterON", state);
  }

  function observeAreas() {
    let areas = [
      'textarea[name="review[body]"]',
      'textarea[name="comment[body]"]',
      'textarea[name="critique[text]"]',
      'textarea[name="club[description]"]',
      'textarea[name="club_page[text]"]',
      'textarea[name="message[body]"]',
      'textarea[name="article[body]"]',
      'textarea[name="anime[description_ru_text]"]',
      'textarea[name="anime[description_en_text]"]',
      'textarea[name="reason"]',
      'textarea[name="character[description_en_text]"]',
      'textarea[name="character[description_ru_text]"]',
      'textarea[name="manga[description_ru_text]"]',
      'textarea[name="manga[description_en_text]"]',
      'textarea[name="collection[text]"]',
    ];

    areas.forEach(function (selector) {
      let textarea = $(selector);
      if (textarea.length) {
        if (formatterON) {
          textarea.on("paste", function (event) {
            vstavka(event, textarea);
          });
        }
        addCounter(textarea);
      }
    });
  }

  function createConfig() {
    const blockContainer = document.querySelector(".block");

    if (
      !document.getElementById("formatterConf") &&
      !document.getElementById("counterConf")
    ) {
      if (blockContainer) {
        const formatterToggle = document.createElement("label");
        formatterToggle.className = "boolean optional control-label checkbox";
        formatterToggle.setAttribute("for", "formatterConf");
        formatterToggle.innerHTML = `
                  <input class="boolean optional" type="checkbox" value="1" id="formatterConf" ${
                    formatterON ? "checked" : ""
                  }>
                  <span>Link Formatter</span>
              `;

        const counterToggle = document.createElement("label");
        counterToggle.className = "boolean optional control-label checkbox";
        counterToggle.setAttribute("for", "counterConf");
        counterToggle.innerHTML = `
                  <input class="boolean optional" type="checkbox" value="1" id="counterConf" ${
                    counterON ? "checked" : ""
                  }>
                  <span>Counter</span>
              `;

        blockContainer.appendChild(formatterToggle);
        blockContainer.appendChild(counterToggle);

        document
          .getElementById("formatterConf")
          .addEventListener("change", (e) => {
            formatterConf(e.target.checked);
          });

        document
          .getElementById("counterConf")
          .addEventListener("change", (e) => {
            counterConf(e.target.checked);
          });
      }
    }
  }

  function ready(fn) {
    document.addEventListener("page:load", fn);
    document.addEventListener("turbolinks:load", fn);
    if (
      document.attachEvent
        ? document.readyState === "complete"
        : document.readyState !== "loading"
    ) {
      fn();
    } else {
      document.addEventListener("DOMContentLoaded", fn);
    }
  }

  ready(function () {
    if (settingsPage()) {
      createConfig();
    } else {
      observeAreas();
    }
  });
})();