Greasy Fork is available in English.
Скрипт для замены номеров телефона на кликабельные ссылки, работает с любыми международными номерами на любых страницах.
// ==UserScript== // @name Универсальные кликабельные номера телефонов на странице (Международные) // @name:en Universal Clickable Phone Numbers (International) // @namespace http://tampermonkey.net/ // @version 2.6 // @description Скрипт для замены номеров телефона на кликабельные ссылки, работает с любыми международными номерами на любых страницах. // @description:en Script to replace phone numbers with clickable links, works with any international numbers on any pages. // @author Coffee_Feather // @match *://*/* // @grant none // @noframes // @license MIT // ==/UserScript== (function() { 'use strict'; const config = { phonePatterns: [ { type: 'RU_FULL', regex: /(?:\+7|8|7)[\s()-]*\d{3}[\s()-]*\d{3}[\s()-]*\d{2}[\s()-]*\d{2}/g, minDigits: 11, maxDigits: 11, normalize: (match) => { let digits = match.replace(/\D/g, ''); digits = digits.startsWith('8') ? '+7' + digits.slice(1) : '+7' + digits.slice(1); return digits; }, disabled: false }, { type: 'UA_FULL', regex: /(?:\+380|380|\+38|38)(?:[\s()-]*\d){9,10}/g, minDigits: 12, maxDigits: 12, normalize: (match) => { let digits = match.replace(/\D/g, ''); // Обработка формата +38XX... if (digits.startsWith('38') && digits.length === 10) { return '+380' + digits.slice(2); } // Обработка формата +380XX... if (digits.startsWith('380')) { return '+' + digits; } // Все остальные случаи считаем невалидными return null; }, disabled: false }, { type: 'INTERNATIONAL', regex: /\+\d{1,4}(?:[\s()-]*\d){6,14}/g, // Разрешены разделители между цифрами minDigits: 7, // Минимальная длина: код страны (1-4) + номер (6+) maxDigits: 15, // Максимальная длина: код (4) + номер (11) normalize: function(match) { const digits = match.replace(/\D/g, ''); const codeLength = digits.match(/^\+\d+/)?.[0].length || 0; // Проверка длины номера (без кода страны) const numberLength = digits.length - codeLength; if (numberLength < 6 || numberLength > 11) return null; return digits; }, disabled: true } ], excludedDomains: [''], allowedParents: ['DIV', 'SPAN', 'P', 'TD', 'LABEL'], forbiddenParents: ['A', 'SCRIPT', 'STYLE', 'TEXTAREA'], forbiddenClasses: ['sidebar', 'sidebar__menu-item', 'compose-button', 'settings'], debounceTime: 300 }; function isForbidden(node) { if (!node) return true; if (node.classList && config.forbiddenClasses.some(c => node.classList.contains(c))) { return true; } if (config.forbiddenParents.includes(node.tagName)) { return true; } let parent = node.parentNode; while (parent && parent !== document.body) { if (parent.classList && config.forbiddenClasses.some(c => parent.classList.contains(c))) { return true; } if (config.forbiddenParents.includes(parent.tagName)) { return true; } parent = parent.parentNode; } return false; } function safeReplace(textNode) { try { const text = textNode.nodeValue; const parent = textNode.parentNode; if (!parent || !config.allowedParents.includes(parent.tagName) || isForbidden(parent)) return; const matches = []; config.phonePatterns.forEach(pattern => { if (pattern.disabled) return; // Пропускаем отключенные модули let match; const regex = new RegExp(pattern.regex.source, 'g'); while ((match = regex.exec(text)) !== null) { const [fullMatch] = match; const digitsOnly = fullMatch.replace(/\D/g, ''); // Проверка длины для конкретного паттерна const length = digitsOnly.length; if (length < pattern.minDigits || length > pattern.maxDigits) continue; // Нормализация номера const normalized = pattern.normalize(fullMatch); matches.push({ original: fullMatch, index: match.index, normalized, type: pattern.type }); } }); if (matches.length === 0) return; // Сортировка и удаление пересечений matches.sort((a, b) => a.index - b.index); const filteredMatches = []; let lastEnd = -1; for (const match of matches) { if (match.index > lastEnd) { filteredMatches.push(match); lastEnd = match.index + match.original.length; } } // Создание DOM элементов const fragment = document.createDocumentFragment(); let lastIndex = 0; filteredMatches.forEach(({original, index, normalized, type}) => { if (index > lastIndex) { fragment.appendChild(document.createTextNode(text.slice(lastIndex, index))); } const link = document.createElement('a'); link.href = `tel:${normalized}`; link.dataset.phoneType = type; link.style.cssText = 'color: inherit; text-decoration: inherit;'; link.textContent = original; fragment.appendChild(link); lastIndex = index + original.length; }); if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.slice(lastIndex))); } parent.replaceChild(fragment, textNode); } catch(e) { console.error('Phone replace error:', e); } } function processor() { // Проверка исключений для домена и всех поддоменов const isExcluded = config.excludedDomains.some(domain => { const host = location.hostname.toLowerCase(); const checkDomain = domain.toLowerCase().replace(/^\./, ''); // Проверка точного совпадения или поддомена return host === checkDomain || host.endsWith(`.${checkDomain}`); }); if (isExcluded) { console.log('Домен исключен:', location.hostname); return; } const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode(node) { if (isForbidden(node.parentElement)) return NodeFilter.FILTER_REJECT; // Фильтруем отключенные паттерны const activePatterns = config.phonePatterns.filter(p => !p.disabled); return activePatterns.some(pattern => { const regex = new RegExp(pattern.regex.source); return regex.test(node.nodeValue); }) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); nodes.reverse().forEach(safeReplace); } function debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // Инициализация if (!config.excludedDomains.includes(location.hostname)) { processor(); const debouncedProcessor = debounce(processor, config.debounceTime); new MutationObserver(debouncedProcessor).observe(document.body, { childList: true, subtree: true, characterData: true }); } })();