Загрузка данных


// ==UserScript==
// @name         Intim Mirrors - Block Images
// @namespace    https://a.intimcity.promo/
// @version      3.4.10
// @description  Fast image/contact blocking and risky-word censoring for intim mirrors
// @author       You
// @match        *://*/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const STORAGE_KEY = "tm_intim_block_images_enabled";
  const TOGGLE_ID = "tm-image-toggle-btn";
  const STYLE_ID = "tm-block-images-style";
  const CONTACT_STYLE_ID = "tm-hide-contacts-style";
  const SEARCH_STYLE_ID = "tm-search-visibility-style";
  const CONTACT_KEEP_SELECTOR = "#headerBg, #headerBg *, .main_link1, .main_link1 *, #search, #search *";
  const USERINFO_KEEP_SELECTOR = ".userinfo-link, .userinfo, [itemprop='author']";
  const COMMENT_TEXTAREA_SELECTOR = "textarea[readonly], textarea[name='comment_text'], textarea[id^='user_comment_']";
  const IMAGE_KEEP_ROOT_SELECTOR = "#headerBg, .main_link1, .up-ocenki-basic, .up-ocenki-dop";
  const COMPARE_TRIGGER_SELECTOR =
    '[class*="sravn"], [id*="sravn"], [class*="compare"], [id*="compare"], [onclick*="sravn"], [onclick*="compare"]';
  const HIDDEN_CONTACT_ATTR = "data-tm-contact-hidden";
  const HOST_RE = /intim/i; // Works for changing mirrors with "intim" in host.
  const CONTACT_LINK_SELECTOR = [
    'a[href^="tel:"]',
    'a[href^="callto:"]',
    'a[href^="sms:"]',
    'a[href*="wa.me/"]',
    'a[href*="api.whatsapp.com"]',
    'a[href*="whatsapp"]',
    'a[href*="viber"]',
    'a[href*="tg://"]',
    'a[href*="t.me/"]',
    '[class*="phone"]',
    '[class*="whatsapp"]',
    '[class*="viber"]',
    '[class*="telegram"]',
    '[id*="phone"]',
    '[id*="whatsapp"]',
    '[id*="viber"]',
    '[id*="telegram"]',
    "[data-phone]",
    "[data-tel]",
    "[data-whatsapp]",
    "[data-viber]",
    "[data-telegram]",
  ].join(", ");
  const CONTACT_ATTR_SCAN_SELECTOR =
    "[href], [onclick], [title], [aria-label], [data-phone], [data-tel], [data-whatsapp], [data-viber], [data-telegram]";
  const CONTACT_ATTR_NAME_RE = /(?:^|[-_:.])(phone|tel|whatsapp|viber|telegram|tg)(?:[-_:.]|$)/i;
  const CONTACT_VALUE_RE =
    /(tel:|callto:|sms:|wa\.me|api\.whatsapp\.com|whatsapp|viber|tg:\/\/|t\.me\/|telegram|\bтелефон\b|\bномер\b|\bphone\b|\btel\b)/i;
  const IMAGE_ATTR_FILTER = ["src", "srcset", "poster", "style", "data-src", "data-srcset"];
  const CONTACT_ATTR_FILTER = [
    "href",
    "onclick",
    "title",
    "aria-label",
    "data-phone",
    "data-tel",
    "data-whatsapp",
    "data-viber",
    "data-telegram",
  ];
  // Max strict mode for short-video platforms.
  // Edit lists only; censor regex is generated automatically.
  // Use "*" at the end to match word forms by stem, e.g. "проститут*" => "проститутка/проститутки".
  const FORBIDDEN_WORDS = {
    adult: [
      "18+",
      "xxx",
      "порн*",
      "porn*",
      "nsfw",
      "эрот*",
      "интим*",
      "адалт*",
      "adult",
      "секс*",
      "cекс*",
      "cек*",
      "sex*",
      "sexy",
      "nude*",
      "nudity",
      "голый",
      "голая",
      "голые",
      "голыш*",
      "обнажен*",
      "обнаж*",
      "фетиш*",
      "bdsm",
      "бдсм",
      "минет*",
      "куни*",
      "аналь*",
      "оральн*",
      "oral*",
      "оргия*",
      "дроч*",
      "мастурб*",
      "онлифанс",
      "onlyfans",
      "only fans",
      "вебкам*",
      "webcam*",
      "camgirl*",
      "cam model*",
      "секс-услуг*",
      "sexwork*",
      "sex work*",
    ],
    escort: [
      "эскорт*",
      "эск##т",
      "escort*",
      "проститут*",
      "проституц*",
      "шлюх*",
      "шалав*",
      "девушка по вызову",
      "девочки по вызову",
      "по вызову",
      "интим услуги",
    ],
    intimacyServices: [
      "классич* секс*",
      "секс класс*",
      "группов* секс*",
      "аналь*",
      "ана##*",
      "минет*",
      "ми##т*",
      "горлов* минет*",
      "кунилингус*",
      "поцелу*",
      "окончание в рот",
      "окончание на лицо",
      "окончание на грудь",
      "член*",
      "глот*",
      "конч*",
      "сквирт*",
      "виртуальн* секс*",
      "секс по телефону",
      "телефонн* секс*",
      "эротическ* массаж*",
      "эро#######*",
      "урологическ* массаж*",
      "лингам*",
      "карсай*",
      "страпон*",
      "анилингус*",
      "золот* дожд*",
      "копро*",
      "фистинг*",
      "фингеринг*",
      "садо-мазо",
      "садо мазо",
      "бандаж*",
      "госпожа*",
      "игры*",
      "легк* доминац*",
      "порк*",
      "рабын*",
      "фетиш*",
      "фе##ш*",
      "футфетиш*",
      "трамплинг*",
      "клизм*",
      "боллбастинг*",
      "ballbusting*",
      "феминизац*",
      "размер имеет значение",
      "услуги семейной паре",
    ],
    violenceSelfHarm: [
      "насили*",
      "изнасил*",
      "убий*",
      "убить",
      "расстрел*",
      "зарез*",
      "задуш*",
      "пытк*",
      "избиени*",
      "жесток*",
      "жесть",
      "мясоруб*",
      "кровав*",
      "труп*",
      "расчлен*",
      "суицид*",
      "самоубий*",
      "самоповрежд*",
      "селфхарм*",
      "selfharm*",
      "self-harm*",
      "suicide*",
      "kill*",
      "murder*",
      "dead body",
      "corpse",
      "повес*",
      "вскрыть вены",
      "резать вены",
      "вены вскры*",
    ],
    drugsAlcoholTobacco: [
      "наркот*",
      "закладк*",
      "закладчик*",
      "драг*",
      "drug*",
      "cocaine",
      "кокаин*",
      "heroin",
      "героин*",
      "meth",
      "метамфет*",
      "амфет*",
      "мефедрон*",
      "mdma",
      "мдма",
      "экстази",
      "ecstasy",
      "спайс*",
      "соль*",
      "марихуан*",
      "конопл*",
      "каннабис*",
      "гашиш*",
      "weed",
      "420",
      "алкогол*",
      "бухл*",
      "бухат*",
      "пьян*",
      "водк*",
      "виски",
      "whiskey",
      "пиво",
      "вино",
      "самогон*",
      "табак*",
      "сигарет*",
      "сигар*",
      "вейп*",
      "vape*",
      "никотин*",
      "smok*",
      "курен*",
    ],
    hateDiscrimination: [
      "расист*",
      "расизм*",
      "нацист*",
      "нацизм*",
      "фаш*",
      "ксенофоб*",
      "дискриминац*",
      "унижен*",
      "оскорбл*",
      "hatespeech",
      "hate speech",
      "slur*",
      "негр*",
      "ниггер*",
      "nigg*",
      "chink*",
      "spic*",
      "kike*",
      "trann*",
      "транн*",
      "fagg*",
      "пидор*",
      "пидр*",
      "гомик*",
      "жидов*",
      "жиден*",
      "хач*",
      "чурк*",
      "blackface",
      "white power",
      "supremac*",
    ],
    crimeScam: [
      "мошенн*",
      "скам*",
      "scam*",
      "обман*",
      "развод*",
      "краж*",
      "грабеж*",
      "угон*",
      "взлом*",
      "hack*",
      "хак*",
      "phishing*",
      "фишинг*",
      "кардинг*",
      "carding*",
      "даркнет*",
      "darknet*",
      "отмыв*",
      "подделк*",
      "фейк док*",
      "подделка документов",
      "вымогат*",
      "шантаж*",
      "террор*",
      "экстрем*",
      "пропаганд* террор",
      "вербовк*",
      "crypto pump",
      "памп*",
      "дамп*",
      "фин пирами*",
      "пирамид*",
      "дропп*",
      "обнал*",
    ],
    weaponsExplosives: [
      "оружи*",
      "огнестрел*",
      "пистолет*",
      "револьвер*",
      "автомат*",
      "штурмовая винтовк*",
      "shotgun*",
      "rifle*",
      "gun*",
      "ammo*",
      "патрон*",
      "нож*",
      "кастет*",
      "гранат*",
      "бомб*",
      "взрыв*",
      "взрывчат*",
      "терракт*",
      "теракт*",
      "стрельб*",
      "поджог*",
    ],
    gamblingBetting: [
      "казино*",
      "casino*",
      "ставк*",
      "бетт*",
      "betting*",
      "букмекер*",
      "лудоман*",
      "gambl*",
      "джекпот*",
      "слот*",
      "рулетк*",
      "тотализатор*",
      "покер*",
    ],
    medicalMisinformation: [
      "чудо-лекарств*",
      "чудо средство",
      "гарантированно лечит",
      "вылечит рак",
      "лечит рак",
      "без врачей",
      "без операций",
      "анорекс*",
      "булими*",
      "быстрое похудение",
      "похудеть за 3 дня",
      "похудеть за неделю на 10",
      "таблетки для похудения",
      "fat burner",
    ],
    minorsAndAbuse: [
      "педоф*",
      "несовершеннолетн*",
      "малолетк*",
      "child porn*",
      "lolicon*",
      "лоликон*",
      "incest*",
      "инцест*",
      "grooming*",
      "груминг*",
    ],
    bloggerEuphemisms: [
      "запрещенк*",
      "деликатная тема",
      "контент 18",
      "клубничк*",
      "клубника",
      "темки 18",
      "серый контент",
      "gray content",
      "грязные деньги",
      "быстрый заработок",
      "легкие деньги",
      "гарантированный доход",
      "100% прибыль",
    ],
    profanityRu: [
      "бля*",
      "бляд*",
      "еб*",
      "ёб*",
      "пизд*",
      "хуй*",
      "хер*",
      "наху*",
      "нахер*",
      "сук*",
      "мраз*",
      "твар*",
      "гандон*",
      "мудак*",
      "долбоеб*",
      "долбаеб*",
      "ублюд*",
      "уеб*",
      "шмар*",
      "сран*",
      "срать*",
      "сру*",
      "засра*",
      "говн*",
      "петух*",
      "чмо*",
    ],
    profanityEn: [
      "fuck*",
      "shit*",
      "bitch*",
      "asshole*",
      "motherf*",
      "cunt*",
      "whore*",
      "slut*",
      "dick*",
      "cock*",
      "pussy*",
      "bastard*",
      "retard*",
      "wtf",
      "stfu",
    ],
  };
  const FORBIDDEN_WORD_TOKENS = Object.values(FORBIDDEN_WORDS).flat();
  const RISKY_WORD_REGEX = compileForbiddenWordRegex(FORBIDDEN_WORD_TOKENS);
  const TEXT_SANITIZE_HINT_RE = compileSanitizeHintRegex(FORBIDDEN_WORD_TOKENS);
  const OBSERVER_ATTR_FILTER = Array.from(new Set([...IMAGE_ATTR_FILTER, ...CONTACT_ATTR_FILTER]));

  if (!HOST_RE.test(location.hostname)) return;

  const blockingEnabled = localStorage.getItem(STORAGE_KEY) !== "0";

  const CSS_BLOCK = `
    img,
    picture,
    source,
    svg image,
    video[poster],
    [style*="background-image"]:not(#headerBg):not(#headerBg *):not(.main_link1):not(.main_link1 *):not(.up-ocenki-basic):not(.up-ocenki-basic *):not(.up-ocenki-dop):not(.up-ocenki-dop *) {
      display: none !important;
      visibility: hidden !important;
      opacity: 0 !important;
    }

    *:not(#headerBg):not(#headerBg *):not(.main_link1):not(.main_link1 *):not(.up-ocenki-basic):not(.up-ocenki-basic *):not(.up-ocenki-dop):not(.up-ocenki-dop *) {
      background-image: none !important;
    }

    .up-ocenki-basic img,
    .up-ocenki-dop img {
      display: inline-block !important;
      visibility: visible !important;
      opacity: 1 !important;
    }
  `;

  const CSS_HIDE_CONTACTS = `
    a[href^="tel:"],
    a[href^="callto:"],
    a[href^="sms:"],
    a[href*="wa.me/"],
    a[href*="api.whatsapp.com"],
    a[href*="whatsapp"],
    a[href*="viber"],
    a[href*="tg://"],
    a[href*="t.me/"],
    [data-phone],
    [data-tel],
    [data-whatsapp],
    [data-viber],
    [data-telegram] {
      display: none !important;
      visibility: hidden !important;
      opacity: 0 !important;
    }
  `;

  const CSS_SEARCH_VISIBILITY = `
    #search,
    #search .searchphone,
    #search form {
      display: block !important;
      visibility: visible !important;
      opacity: 1 !important;
    }

    #search #searchStr {
      background: #ffffff !important;
      color: #111111 !important;
      border: 1px solid #777777 !important;
      border-radius: 4px !important;
      padding: 4px 8px !important;
    }

    #search #searchPS {
      display: inline-block !important;
      visibility: visible !important;
      opacity: 1 !important;
      min-width: 74px !important;
      min-height: 28px !important;
      padding: 4px 10px !important;
      border: 1px solid #666666 !important;
      border-radius: 4px !important;
      background: #f2f2f2 !important;
      color: #111111 !important;
      cursor: pointer !important;
      text-indent: 0 !important;
      line-height: 1.2 !important;
    }

    #search #menuRight2 {
      color: #f2f2f2 !important;
    }

    #search #menuRight2 a.menu {
      color: #f2f2f2 !important;
      text-decoration: underline !important;
    }

    #search #menuRight2 a.menu[style*="#CC0000"],
    #search #menuRight2 a.menu[style*="#cc0000"] {
      color: #ff5959 !important;
    }
  `;

  function appendNode(node) {
    const root = document.documentElement || document.head || document.body;
    if (root) {
      root.appendChild(node);
      return;
    }

    document.addEventListener(
      "readystatechange",
      () => {
        const nextRoot = document.documentElement || document.head || document.body;
        if (nextRoot && !node.isConnected) nextRoot.appendChild(node);
      },
      { once: true }
    );
  }

  function setBlockingState(enabled) {
    localStorage.setItem(STORAGE_KEY, enabled ? "1" : "0");
  }

  function injectToggleButton() {
    if (document.getElementById(TOGGLE_ID)) return;

    const style = document.createElement("style");
    style.textContent = `
      #${TOGGLE_ID} {
        position: fixed !important;
        right: 12px !important;
        bottom: 12px !important;
        z-index: 2147483647 !important;
        padding: 8px 12px !important;
        border: 1px solid #333 !important;
        border-radius: 8px !important;
        font: 12px/1.2 Arial, sans-serif !important;
        cursor: pointer !important;
        color: #fff !important;
        background: ${blockingEnabled ? "#b00020" : "#0a7a2f"} !important;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35) !important;
      }
      #${TOGGLE_ID}:hover {
        filter: brightness(1.08) !important;
      }
    `;
    appendNode(style);

    const btn = document.createElement("button");
    btn.id = TOGGLE_ID;
    btn.type = "button";
    btn.textContent = blockingEnabled ? "Изображения: OFF" : "Изображения: ON";
    btn.title = "Переключить и перезагрузить";
    btn.addEventListener("click", () => {
      setBlockingState(!blockingEnabled);
      location.reload();
    });

    const mount = () => {
      if (!document.body) return;
      document.body.appendChild(btn);
    };

    if (document.body) {
      mount();
    } else {
      document.addEventListener("DOMContentLoaded", mount, { once: true });
    }
  }

  function injectBlockCss() {
    if (document.getElementById(STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = CSS_BLOCK;
    appendNode(style);
  }

  function injectContactCss() {
    if (document.getElementById(CONTACT_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = CONTACT_STYLE_ID;
    style.textContent = CSS_HIDE_CONTACTS;
    appendNode(style);
  }

  function injectSearchCss() {
    if (document.getElementById(SEARCH_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = SEARCH_STYLE_ID;
    style.textContent = CSS_SEARCH_VISIBILITY;
    appendNode(style);
  }

  function repairSearchUi(root) {
    if (!(root instanceof Document || root instanceof Element)) return;
    const btn = root.querySelector("#searchPS");
    if (btn instanceof HTMLInputElement && btn.type === "submit") {
      if (!btn.value || !btn.value.trim()) {
        btn.value = "Найти";
      }
    }
  }

  function escapeForRegex(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  function compileForbiddenWordRegex(tokens) {
    const uniq = Array.from(new Set(tokens.map((v) => (v || "").trim().toLowerCase()).filter(Boolean)));
    const variants = [];

    for (const token of uniq) {
      const isStem = token.endsWith("*");
      const base = isStem ? token.slice(0, -1) : token;
      if (!base) continue;

      const escaped = escapeForRegex(base);
      const variant = isStem ? `${escaped}[\\p{L}\\p{N}_-]*` : escaped;
      variants.push(variant);
    }

    if (!variants.length) return null;
    return new RegExp(`(?<![\\p{L}\\p{N}_])(?:${variants.join("|")})(?![\\p{L}\\p{N}_])`, "giu");
  }

  function compileSanitizeHintRegex(tokens) {
    const chunks = Array.from(
      new Set(
        tokens
          .map((v) => (v || "").trim().toLowerCase().replace(/\*+$/g, ""))
          .filter(Boolean)
          .map(escapeForRegex)
      )
    );

    if (!chunks.length) return /\d/;
    return new RegExp(`\\d|${chunks.join("|")}`, "i");
  }

  function hasPhoneLikePattern(value) {
    if (!value || !/\d/.test(value)) return false;
    const hasPhoneCue = /(тел|телефон|phone|tel|номер|call|wa\.me|whatsapp|viber|telegram|tg)/i.test(value);
    const matches = value.match(/(?:\+?\d[\d\s().-]{8,}\d)/g);
    if (!matches) return false;
    return matches.some((chunk) => {
      const digits = chunk.replace(/\D/g, "");
      if (digits.length < 10 || digits.length > 15) return false;
      const hasDelimiter = /[\s().-]/.test(chunk) || chunk.startsWith("+");
      return hasDelimiter || hasPhoneCue;
    });
  }

  function maskPhoneText(value) {
    return value.replace(/(?:\+?\d[\d\s().-]{8,}\d)/g, (chunk) => {
      const digits = chunk.replace(/\D/g, "");
      if (digits.length < 10 || digits.length > 15) return chunk;
      return "номер скрыт";
    });
  }

  function maskTokenWithHashes(token) {
    const chars = Array.from(token);
    const letterLikeIndexes = [];

    for (let i = 0; i < chars.length; i += 1) {
      if (/[\p{L}\p{N}]/u.test(chars[i])) {
        letterLikeIndexes.push(i);
      }
    }

    if (letterLikeIndexes.length <= 2) {
      if (letterLikeIndexes.length === 0) return token;
      if (letterLikeIndexes.length === 1) return "#";
      chars[letterLikeIndexes[1]] = "#";
      return chars.join("");
    }

    // Keep readability for "конч*" forms (e.g. "кончил" -> "к#нчил").
    if (/^конч/iu.test(token) && letterLikeIndexes.length >= 2) {
      const secondLetterIndex = letterLikeIndexes[1];
      chars[secondLetterIndex] = "#";
      return chars.join("");
    }

    // Keep readability for "член*" forms (e.g. "член" -> "чл#н").
    if (/^член/iu.test(token) && letterLikeIndexes.length >= 3) {
      const thirdLetterIndex = letterLikeIndexes[2];
      chars[thirdLetterIndex] = "#";
      return chars.join("");
    }

    // Soft mode: keep words understandable by masking only 1 char (2 for very long tokens).
    const logicalMaskIndexes = [1];
    if (letterLikeIndexes.length >= 10) {
      logicalMaskIndexes.push(Math.floor(letterLikeIndexes.length / 2));
    }

    for (const logicalIndex of logicalMaskIndexes) {
      const sourceIndex = letterLikeIndexes[logicalIndex];
      if (sourceIndex === undefined) continue;
      chars[sourceIndex] = "#";
    }

    return chars.join("");
  }

  function maskWordWithHashes(value) {
    const tokenMatches = Array.from(value.matchAll(/[\p{L}\p{N}]+/gu));
    if (!tokenMatches.length) return value;

    if (tokenMatches.length === 1) {
      return value.replace(/[\p{L}\p{N}]+/u, (token) => maskTokenWithHashes(token));
    }

    // For multi-word phrases, mask only the first word to keep text understandable.
    const first = tokenMatches[0];
    const start = first.index || 0;
    const token = first[0];
    const masked = maskTokenWithHashes(token);
    return `${value.slice(0, start)}${masked}${value.slice(start + token.length)}`;
  }

  function maskRiskyWords(value) {
    if (!RISKY_WORD_REGEX) return value;
    RISKY_WORD_REGEX.lastIndex = 0;
    return value.replace(RISKY_WORD_REGEX, (word) => maskWordWithHashes(word));
  }

  function sanitizeText(value) {
    if (!value || !TEXT_SANITIZE_HINT_RE.test(value)) return value;
    let next = maskPhoneText(value);
    next = maskRiskyWords(next);
    return next;
  }

  function isInsideImageKeepArea(el) {
    if (!(el instanceof Element)) return false;
    if (el.matches(IMAGE_KEEP_ROOT_SELECTOR)) return true;
    return Boolean(el.closest(IMAGE_KEEP_ROOT_SELECTOR));
  }

  function isInsideContactKeepArea(el) {
    if (!(el instanceof Element)) return false;
    if (el.matches(CONTACT_KEEP_SELECTOR)) return true;
    return Boolean(el.closest("#headerBg, .main_link1, #search"));
  }

  function isUserInfoElement(el) {
    if (!(el instanceof Element)) return false;
    if (el.matches(USERINFO_KEEP_SELECTOR)) return true;
    return Boolean(el.closest(USERINFO_KEEP_SELECTOR));
  }

  function hideContactElement(el) {
    if (!(el instanceof Element)) return;
    if (isInsideContactKeepArea(el) || isUserInfoElement(el)) {
      if (el.hasAttribute(HIDDEN_CONTACT_ATTR)) {
        el.removeAttribute(HIDDEN_CONTACT_ATTR);
        el.style.removeProperty("display");
        el.style.removeProperty("visibility");
        el.style.removeProperty("opacity");
      }
      return;
    }
    if (el.hasAttribute(HIDDEN_CONTACT_ATTR)) return;
    el.setAttribute(HIDDEN_CONTACT_ATTR, "1");
    el.style.setProperty("display", "none", "important");
    el.style.setProperty("visibility", "hidden", "important");
    el.style.setProperty("opacity", "0", "important");
    if (el.tagName === "A") {
      el.removeAttribute("href");
    }
    el.removeAttribute("onclick");
  }

  function shouldHideByAttributes(el) {
    if (!(el instanceof Element)) return false;
    if (isUserInfoElement(el)) return false;
    const attrNames = el.getAttributeNames();
    for (const attrName of attrNames) {
      const value = (el.getAttribute(attrName) || "").trim();
      const name = attrName.toLowerCase();
      const allowPhonePattern =
        name === "href" ||
        name === "title" ||
        name === "aria-label" ||
        name === "value" ||
        name.startsWith("data-");

      if (CONTACT_ATTR_NAME_RE.test(name) && value) return true;
      if (value && CONTACT_VALUE_RE.test(value)) return true;
      if (value && allowPhonePattern && hasPhoneLikePattern(value)) return true;
    }
    return false;
  }

  function sanitizeTextNode(node) {
    if (!(node instanceof Text)) return;
    if (!node.nodeValue) return;
    const parent = node.parentElement;
    if (!parent) return;
    const tag = parent.tagName;
    if (tag === "SCRIPT" || tag === "STYLE" || tag === "NOSCRIPT" || tag === "TEXTAREA") return;

    const next = sanitizeText(node.nodeValue);
    if (next !== node.nodeValue) {
      node.nodeValue = next;
    }
  }

  function scanSanitizedText(root) {
    if (!(root instanceof Document || root instanceof Element)) return;
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    let node = walker.nextNode();
    while (node) {
      sanitizeTextNode(node);
      node = walker.nextNode();
    }
  }

  function sanitizeCommentTextarea(el) {
    if (!(el instanceof HTMLTextAreaElement)) return;
    const source = el.value || el.textContent || "";
    if (!source) return;

    const next = sanitizeText(source);
    if (next === source) return;

    el.value = next;
    el.textContent = next;
  }

  function scanSanitizedCommentFields(root) {
    if (!(root instanceof Document || root instanceof Element)) return;

    if (root instanceof HTMLTextAreaElement && root.matches(COMMENT_TEXTAREA_SELECTOR)) {
      sanitizeCommentTextarea(root);
    }

    root.querySelectorAll(COMMENT_TEXTAREA_SELECTOR).forEach((el) => {
      sanitizeCommentTextarea(el);
    });
  }

  function forEachMatch(root, selector, cb) {
    if (!(root instanceof Document || root instanceof Element)) return;
    if (root instanceof Element && root.matches(selector)) cb(root);
    root.querySelectorAll(selector).forEach(cb);
  }

  function scanContacts(root) {
    if (!(root instanceof Document || root instanceof Element)) return;

    forEachMatch(root, CONTACT_LINK_SELECTOR, hideContactElement);
    forEachMatch(root, CONTACT_ATTR_SCAN_SELECTOR, (el) => {
      if (shouldHideByAttributes(el)) hideContactElement(el);
    });
    scanSanitizedText(root);
    scanSanitizedCommentFields(root);
  }

  function neutralize(el) {
    if (!(el instanceof Element)) return;

    if (!isInsideContactKeepArea(el)) {
      if (el.matches(CONTACT_LINK_SELECTOR) || shouldHideByAttributes(el)) {
        hideContactElement(el);
      }
    }

    if (!blockingEnabled) return;
    if (isInsideImageKeepArea(el)) return;

    if (el.matches("img")) {
      el.removeAttribute("src");
      el.removeAttribute("srcset");
      el.removeAttribute("data-src");
      el.removeAttribute("data-srcset");
      el.removeAttribute("data-original");
      el.removeAttribute("data-lazy");
      el.removeAttribute("data-lazy-src");
      return;
    }

    if (el.matches("source")) {
      el.removeAttribute("src");
      el.removeAttribute("srcset");
      return;
    }

    if (el.matches("video[poster]")) {
      el.removeAttribute("poster");
    }

    if (el.hasAttribute("style")) {
      const style = el.getAttribute("style") || "";
      if (/background-image\s*:\s*url\(/i.test(style)) {
        el.setAttribute(
          "style",
          style.replace(/background-image\s*:[^;]+;?/gi, "background-image:none !important;")
        );
      }
    }
  }

  function scanImages(root) {
    if (!(root instanceof Document || root instanceof Element)) return;
    const list = root.querySelectorAll(
      "img, source, picture, svg image, video[poster], [style*='background-image']:not(#headerBg):not(#headerBg *):not(.main_link1):not(.main_link1 *):not(.up-ocenki-basic):not(.up-ocenki-basic *):not(.up-ocenki-dop):not(.up-ocenki-dop *)"
    );
    list.forEach(neutralize);
  }

  function scan(root) {
    if (!(root instanceof Document || root instanceof Element)) return;
    scanContacts(root);
    repairSearchUi(root);
    if (blockingEnabled) scanImages(root);
  }

  function triggerCompareRescan() {
    setTimeout(() => scan(document), 0);
    setTimeout(() => scan(document), 150);
    setTimeout(() => scan(document), 450);
  }

  function observeCompareTriggers() {
    document.addEventListener(
      "click",
      (event) => {
        if (!(event.target instanceof Element)) return;
        if (!event.target.closest(COMPARE_TRIGGER_SELECTOR)) return;
        triggerCompareRescan();
      },
      true
    );
  }

  function observe() {
    const root = document.documentElement;
    if (!root) {
      document.addEventListener("DOMContentLoaded", observe, { once: true });
      return;
    }

    const obs = new MutationObserver((mutations) => {
      for (const m of mutations) {
        if (m.type === "characterData") {
          sanitizeTextNode(m.target);
          continue;
        }

        if (m.type === "attributes" && m.target instanceof Element) {
          neutralize(m.target);
          continue;
        }

        if (m.type === "childList") {
          m.addedNodes.forEach((n) => {
            if (n instanceof Element) {
              neutralize(n);
              scan(n);
              return;
            }

            if (n instanceof Text) {
              sanitizeTextNode(n);
            }
          });
        }
      }
    });

    obs.observe(root, {
      childList: true,
      subtree: true,
      characterData: true,
      attributes: true,
      attributeFilter: OBSERVER_ATTR_FILTER,
    });
  }

  injectToggleButton();
  injectContactCss();
  injectSearchCss();
  scanContacts(document);
  repairSearchUi(document);
  observeCompareTriggers();
  observe();

  if (blockingEnabled) {
    injectBlockCss();
    scanImages(document);
  }

  document.addEventListener("DOMContentLoaded", () => {
    scan(document);
  });
})();