网页元素屏蔽器

屏蔽任意网站上的元素,支持缩略图记录和正则/简单模式屏蔽,新增动态屏蔽

// ==UserScript==
// @name         网页元素屏蔽器
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  屏蔽任意网站上的元素,支持缩略图记录和正则/简单模式屏蔽,新增动态屏蔽
// @author       JerryChiang
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 添加样式
    const style = document.createElement('style');
    style.textContent = `
        .highlight {
            outline: 2px solid red !important;
            background-color: rgba(255, 0, 0, 0.1) !important;
        }
        .blocker-popup {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border: 1px solid #ccc;
            z-index: 9999;
            box-shadow: 0 0 10px rgba(0,0,0,0.3);
            border-radius: 8px;
            font-family: Arial, sans-serif;
            width: 600px;
            max-height: 80vh;
            overflow-y: auto;
        }
        .blocker-popup p {
            margin: 0 0 10px;
            font-size: 16px;
            color: #333;
        }
        .blocker-popup button {
            margin: 5px;
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s;
        }
        .blocker-popup button:hover {
            opacity: 0.9;
        }
        #static-block {
            background-color: #007bff;
            color: white;
        }
        #dynamic-block {
            background-color: #28a745;
            color: white;
        }
        #preview {
            background-color: #17a2b8;
            color: white;
        }
        #cancel {
            background-color: #dc3545;
            color: white;
        }
        .blocker-popup input, .blocker-popup select {
            margin: 5px;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        }
        .rule-row {
            display: flex;
            align-items: center;
            margin: 5px 0;
            padding: 5px;
            border-bottom: 1px solid #eee;
        }
        .blocker-list {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border: 1px solid #ccc;
            z-index: 9999;
            max-height: 80vh;
            overflow-y: auto;
            width: 500px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0,0,0,0.3);
        }
        .blocker-list h3 {
            margin: 0 0 15px;
            font-size: 18px;
            color: #333;
        }
        .blocker-list ul {
            list-style: none;
            padding: 0;
        }
        .blocker-list li {
            margin: 10px 0;
            display: flex;
            align-items: center;
            width: 100%;
            padding: 5px;
            border-bottom: 1px solid #eee;
        }
        .blocker-list img {
            max-width: 400px;
            max-height: 100px;
            object-fit: contain;
            border: 1px solid #ddd;
            flex-shrink: 0;
        }
        .blocker-list button {
            margin-left: auto;
            flex-shrink: 0;
            padding: 5px 10px;
            background-color: #dc3545;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .blocker-list button:hover {
            background-color: #c82333;
        }
    `;
    document.head.appendChild(style);

    // 注册菜单
    GM_registerMenuCommand('手动屏蔽', startBlockingMode);
    GM_registerMenuCommand('按规则屏蔽', showRegexBlockInput);
    GM_registerMenuCommand('查看屏蔽记录', showBlockList);
    GM_registerMenuCommand('清除当前域名屏蔽规则(点击后需要快速点击回车)', clearDomainBlocks);

    // 进入元素选择模式
    function startBlockingMode() {
        document.body.addEventListener('mouseover', highlightElement);
        document.body.addEventListener('click', selectElement, true);
    }

    // 高亮悬停元素
    function highlightElement(event) {
        if (window.lastHighlighted) {
            window.lastHighlighted.classList.remove('highlight');
        }
        event.target.classList.add('highlight');
        window.lastHighlighted = event.target;
    }

    // 选择元素并弹出确认窗口
    function selectElement(event) {
        event.preventDefault();
        event.stopPropagation();

        document.body.removeEventListener('mouseover', highlightElement);
        document.body.removeEventListener('click', selectElement, true);

        const selectedElement = event.target;
        window.lastHighlighted.classList.remove('highlight');
        showConfirmation(selectedElement);
    }

    // 显示确认弹窗
    function showConfirmation(element) {
        const popup = document.createElement('div');
        popup.className = 'blocker-popup';
        popup.innerHTML = `
            <p>选择屏蔽方式:</p>
            <button id="static-block">静态屏蔽</button>
            <button id="dynamic-block">动态屏蔽</button>
            <button id="preview">预览</button>
            <button id="cancel">取消</button>
        `;
        document.body.appendChild(popup);

        let isPreviewHidden = false;

        const staticBtn = document.getElementById('static-block');
        staticBtn.addEventListener('click', async () => {
            staticBtn.disabled = true;
            try {
                await saveBlockWithThumbnail(element, 'static');
                element.style.display = 'none';
                document.body.removeChild(popup);
            } catch (e) {
                console.error('静态屏蔽失败:', e);
                staticBtn.disabled = false;
            }
        }, { once: true });

        const dynamicBtn = document.getElementById('dynamic-block');
        dynamicBtn.addEventListener('click', async () => {
            dynamicBtn.disabled = true;
            try {
                await saveBlockWithThumbnail(element, 'dynamic');
                applyDynamicBlocks();
                document.body.removeChild(popup);
            } catch (e) {
                console.error('动态屏蔽失败:', e);
                dynamicBtn.disabled = false;
            }
        }, { once: true });

        document.getElementById('preview').addEventListener('click', () => {
            if (!isPreviewHidden) {
                element.style.display = 'none';
                isPreviewHidden = true;
            } else {
                element.style.display = '';
                isPreviewHidden = false;
            }
        });

        document.getElementById('cancel').addEventListener('click', () => {
            document.body.removeChild(popup);
        });
    }

    // 保存屏蔽信息并生成缩略图
    async function saveBlockWithThumbnail(element, type = 'static') {
        const domain = window.location.hostname;
        const selector = getSelector(element);

        const rect = element.getBoundingClientRect();
        const width = rect.width;
        const height = rect.height;
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = width;
        canvas.height = height;

        let thumbnail = null;
        try {
            const computedStyle = window.getComputedStyle(element);
            ctx.fillStyle = computedStyle.backgroundColor || '#ffffff';
            ctx.fillRect(0, 0, width, height);
            if (element.textContent.trim()) {
                ctx.fillStyle = computedStyle.color || '#000000';
                ctx.font = `${computedStyle.fontSize || '16px'} ${computedStyle.fontFamily || 'Arial'}`;
                ctx.textAlign = 'left';
                ctx.textBaseline = 'top';
                const paddingLeft = parseInt(computedStyle.paddingLeft) || 0;
                const paddingTop = parseInt(computedStyle.paddingTop) || 0;
                wrapText(ctx, element.textContent.trim(), paddingLeft, paddingTop, width - paddingLeft * 2, parseInt(computedStyle.fontSize) || 16);
            }
            let scale = Math.min(400 / width, 100 / height, 1);
            const thumbnailCanvas = document.createElement('canvas');
            thumbnailCanvas.width = width * scale;
            thumbnailCanvas.height = height * scale;
            const thumbnailCtx = thumbnailCanvas.getContext('2d');
            thumbnailCtx.drawImage(canvas, 0, 0, thumbnailCanvas.width, thumbnailCanvas.height);
            thumbnail = thumbnailCanvas.toDataURL('image/png');
        } catch (e) {
            console.error('生成缩略图失败:', e);
        }

        if (type === 'static') {
            let blocks = GM_getValue('blocks', {});
            if (!blocks[domain]) blocks[domain] = [];
            if (!blocks[domain].some(item => item.selector === selector)) {
                blocks[domain].push({ selector, thumbnail: thumbnail || null, type: 'static' });
                GM_setValue('blocks', blocks);
            }
        } else if (type === 'dynamic') {
            const className = element.className.split(' ').filter(c => c)[0] || '';
            let dynamicBlocks = GM_getValue('dynamicBlocks', {});
            if (!dynamicBlocks[domain]) dynamicBlocks[domain] = [];
            if (!dynamicBlocks[domain].some(item => item.className === className)) {
                dynamicBlocks[domain].push({ className, thumbnail: thumbnail || null, type: 'dynamic' });
                GM_setValue('dynamicBlocks', dynamicBlocks);
            }
        }
        return true;
    }

    // 辅助函数:将文本换行绘制到 canvas
    function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
        const words = text.split(' ');
        let line = '';
        let currentY = y;
        for (let i = 0; i < words.length; i++) {
            const testLine = line + words[i] + ' ';
            const metrics = ctx.measureText(testLine);
            const testWidth = metrics.width;
            if (testWidth > maxWidth && i > 0) {
                ctx.fillText(line, x, currentY);
                line = words[i] + ' ';
                currentY += lineHeight;
            } else {
                line = testLine;
            }
        }
        ctx.fillText(line, x, currentY);
    }

    // 生成简单 CSS 选择器
    function getSelector(element) {
        if (element.id) return `#${element.id}`;
        let path = [];
        while (element && element.nodeType === Node.ELEMENT_NODE) {
            let selector = element.tagName.toLowerCase();
            if (element.className && typeof element.className === 'string') {
                selector += '.' + element.className.trim().replace(/\s+/g, '.');
            }
            path.unshift(selector);
            element = element.parentElement;
        }
        return path.join(' > ');
    }

    // 应用屏蔽规则(静态 + 动态 + 正则)
    function applyBlocks() {
        const domain = window.location.hostname;
        const blocks = GM_getValue('blocks', {});
        const dynamicBlocks = GM_getValue('dynamicBlocks', {});
        const regexBlocks = GM_getValue('regexBlocks', {});

        // 应用静态屏蔽
        if (blocks[domain]) {
            blocks[domain].forEach(item => {
                try {
                    document.querySelectorAll(item.selector).forEach(el => {
                        if (!el.closest('.blocker-popup') && !el.closest('.blocker-list')) {
                            el.style.display = 'none';
                        }
                    });
                } catch (e) {
                    console.error(`无法应用选择器: ${item.selector}`, e);
                }
            });
        }

        // 应用动态屏蔽
        if (dynamicBlocks[domain]) {
            dynamicBlocks[domain].forEach(rule => {
                const elements = document.getElementsByClassName(rule.className);
                Array.from(elements).forEach(el => {
                    if (!el.closest('.blocker-popup') && !el.closest('.blocker-list')) {
                        el.style.display = 'none';
                    }
                });
            });
        }

        // 应用正则屏蔽
        if (regexBlocks[domain]) {
            regexBlocks[domain].forEach(rule => {
                try {
                    const regex = new RegExp(rule.regex);
                    const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
                        acceptNode: (node) => {
                            const parent = node.parentElement;
                            return (parent && (parent.closest('.blocker-popup') || parent.closest('.blocker-list')))
                                ? NodeFilter.FILTER_REJECT
                                : NodeFilter.FILTER_ACCEPT;
                        }
                    }, false);
                    let node;
                    while (node = walker.nextNode()) {
                        if (regex.test(node.textContent)) {
                            let element = node.parentElement;
                            for (let i = 0; i < rule.level; i++) {
                                if (element.parentElement) element = element.parentElement;
                                else break;
                            }
                            element.style.display = 'none';
                        }
                    }
                } catch (e) {
                    console.error(`无法应用规则: ${rule.regex}`, e);
                }
            });
        }
    }

    // 应用动态屏蔽(独立函数已无必要,保留为空兼容旧调用)
    function applyDynamicBlocks() {
        applyBlocks(); // 直接调用统一应用函数
    }

    // 显示屏蔽记录窗口
    function showBlockList() {
        const domain = window.location.hostname;
        const blocks = GM_getValue('blocks', {});
        const dynamicBlocks = GM_getValue('dynamicBlocks', {});
        const blockList = (blocks[domain] || []).concat(dynamicBlocks[domain] || []);

        const listWindow = document.createElement('div');
        listWindow.className = 'blocker-list';
        listWindow.innerHTML = `
            <h3>当前域名屏蔽记录 (${domain})</h3>
            <ul id="block-items"></ul>
            <button id="close-list">关闭</button>
        `;
        document.body.appendChild(listWindow);

        const ul = document.getElementById('block-items');
        if (blockList.length === 0) {
            ul.innerHTML = '<li>暂无屏蔽记录</li>';
        } else {
            blockList.forEach((item, index) => {
                const li = document.createElement('li');
                if (item.thumbnail) {
                    const img = document.createElement('img');
                    img.src = item.thumbnail;
                    li.appendChild(img);
                } else {
                    li.textContent = item.type === 'static' ? item.selector : item.className;
                }
                const unblockBtn = document.createElement('button');
                unblockBtn.textContent = '取消屏蔽';
                unblockBtn.addEventListener('click', () => {
                    removeBlock(domain, index, item.type);
                    listWindow.remove();
                    applyBlocks();
                    showBlockList();
                });
                li.appendChild(unblockBtn);
                ul.appendChild(li);
            });
        }

        document.getElementById('close-list').addEventListener('click', () => {
            document.body.removeChild(listWindow);
        });
    }

    // 删除屏蔽记录
    function removeBlock(domain, index, type) {
        if (type === 'static') {
            let blocks = GM_getValue('blocks', {});
            if (blocks[domain] && blocks[domain][index]) {
                blocks[domain].splice(index, 1);
                if (blocks[domain].length === 0) delete blocks[domain];
                GM_setValue('blocks', blocks);
            }
        } else if (type === 'dynamic') {
            let dynamicBlocks = GM_getValue('dynamicBlocks', {});
            const staticCount = (GM_getValue('blocks', {})[domain] || []).length;
            const dynamicIndex = index - staticCount;
            if (dynamicBlocks[domain] && dynamicBlocks[domain][dynamicIndex]) {
                dynamicBlocks[domain].splice(dynamicIndex, 1);
                if (dynamicBlocks[domain].length === 0) delete dynamicBlocks[domain];
                GM_setValue('dynamicBlocks', dynamicBlocks);
            }
        }
    }

    // 显示规则屏蔽输入和管理窗口
    function showRegexBlockInput() {
        const domain = window.location.hostname;
        let regexBlocks = GM_getValue('regexBlocks', {});
        let rules = regexBlocks[domain] || [];

        const popup = document.createElement('div');
        popup.className = 'blocker-popup';
        popup.innerHTML = `
            <p>设置屏蔽规则(层级:0 表示当前元素,1 表示父元素,依此类推,请先点击预览,避免失误将整个页面清除):</p>
            <button id="regex-mode">正则模式</button>
            <button id="simple-mode">简单模式</button>
            <div id="input-container"></div>
            <div id="rules-list">
                <h3>当前规则列表</h3>
                <div id="rules-rows"></div>
            </div>
            <div>
                <button id="add-rule-row">新增规则</button>
                <button id="save-rules">保存</button>
                <button id="cancel-rule">取消</button>
            </div>
        `;
        document.body.appendChild(popup);

        document.getElementById('regex-mode').style.backgroundColor = '#007bff';
        document.getElementById('regex-mode').style.color = 'white';
        document.getElementById('simple-mode').style.backgroundColor = '#007bff';
        document.getElementById('simple-mode').style.color = 'white';
        document.getElementById('add-rule-row').style.backgroundColor = '#17a2b8';
        document.getElementById('add-rule-row').style.color = 'white';
        document.getElementById('save-rules').style.backgroundColor = '#17a2b8';
        document.getElementById('save-rules').style.color = 'white';
        document.getElementById('cancel-rule').style.backgroundColor = '#dc3545';
        document.getElementById('cancel-rule').style.color = 'white';

        let isDragging = false;
        let currentX;
        let currentY;
        let initialX;
        let initialY;

        popup.addEventListener('mousedown', (e) => {
            if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'SELECT') {
                isDragging = true;
                initialX = e.clientX - currentX;
                initialY = e.clientY - currentY;
                popup.style.cursor = 'grabbing';
            }
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                e.preventDefault();
                currentX = e.clientX - initialX;
                currentY = e.clientY - initialY;
                popup.style.left = `${currentX + (popup.offsetWidth / 2)}px`;
                popup.style.top = `${currentY + (popup.offsetHeight / 2)}px`;
                popup.style.transform = 'none';
            }
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            popup.style.cursor = 'default';
        });

        currentX = window.innerWidth / 2 - popup.offsetWidth / 2;
        currentY = window.innerHeight / 2 - popup.offsetHeight / 2;
        popup.style.left = `${currentX + (popup.offsetWidth / 2)}px`;
        popup.style.top = `${currentY + (popup.offsetHeight / 2)}px`;

        const inputContainer = document.getElementById('input-container');
        const rulesRows = document.getElementById('rules-rows');
        let isSimpleMode = false;
        let tempRules = [...rules];

        function showRegexInput() {
            inputContainer.innerHTML = `
                <div class="rule-row">
                    <input type="text" class="regex-input" placeholder="正则规则" />
                    <input type="number" class="level-input" placeholder="层级" value="0" min="0" />
                    <button class="preview-rule">预览</button>
                </div>
            `;
            document.querySelector('.preview-rule').style.backgroundColor = '#28a745';
            document.querySelector('.preview-rule').style.color = 'white';
            attachPreviewListeners();
        }

        function showSimpleInput() {
            inputContainer.innerHTML = `
                <div class="rule-row">
                    <select class="logic-select">
                        <option value="contains">包含</option>
                        <option value="not-contains">不包含</option>
                        <option value="equals">等于</option>
                    </select>
                    <input type="text" class="simple-input" placeholder="文本内容" />
                    <input type="number" class="level-input" placeholder="层级" value="0" min="0" />
                    <button class="preview-rule">预览</button>
                </div>
            `;
            document.querySelector('.preview-rule').style.backgroundColor = '#28a745';
            document.querySelector('.preview-rule').style.color = 'white';
            attachPreviewListeners();
        }

        document.getElementById('regex-mode').addEventListener('click', () => {
            isSimpleMode = false;
            showRegexInput();
        });
        document.getElementById('simple-mode').addEventListener('click', () => {
            isSimpleMode = true;
            showSimpleInput();
        });

        showRegexInput();

        function renderRules() {
            rulesRows.innerHTML = '';
            if (tempRules.length === 0) {
                rulesRows.innerHTML = '<p>暂无规则</p>';
                return;
            }
            tempRules.forEach((rule, index) => {
                const row = document.createElement('div');
                row.className = 'rule-row';
                row.innerHTML = `
                    <input type="text" class="rule-regex" value="${rule.regex}" />
                    <input type="number" class="rule-level" value="${rule.level}" min="0" />
                    <button class="delete-rule">删除</button>
                `;
                const regexInput = row.querySelector('.rule-regex');
                const levelInput = row.querySelector('.rule-level');
                const deleteBtn = row.querySelector('.delete-rule');

                regexInput.addEventListener('input', () => {
                    tempRules[index].regex = regexInput.value;
                });
                levelInput.addEventListener('input', () => {
                    tempRules[index].level = parseInt(levelInput.value, 10);
                });

                deleteBtn.addEventListener('click', () => {
                    tempRules.splice(index, 1);
                    renderRules();
                });

                rulesRows.appendChild(row);
            });
        }

        document.getElementById('add-rule-row').addEventListener('click', () => {
            let regex, level;
            if (isSimpleMode) {
                const logic = inputContainer.querySelector('.logic-select').value;
                const text = inputContainer.querySelector('.simple-input').value.trim();
                level = parseInt(inputContainer.querySelector('.level-input').value, 10);
                if (!text || isNaN(level) || level < 0) {
                    alert('请输入有效的文本和层级');
                    return;
                }
                regex = convertSimpleToRegex(logic, text);
            } else {
                regex = inputContainer.querySelector('.regex-input').value.trim();
                level = parseInt(inputContainer.querySelector('.level-input').value, 10);
                if (!regex || isNaN(level) || level < 0) {
                    alert('请输入有效的正则规则和层级');
                    return;
                }
            }
            tempRules.push({ regex, level });
            renderRules();
            if (isSimpleMode) {
                inputContainer.querySelector('.simple-input').value = '';
                inputContainer.querySelector('.level-input').value = '0';
            } else {
                inputContainer.querySelector('.regex-input').value = '';
                inputContainer.querySelector('.level-input').value = '0';
            }
        });

        document.getElementById('save-rules').addEventListener('click', () => {
            regexBlocks[domain] = tempRules;
            GM_setValue('regexBlocks', regexBlocks);
            applyBlocks();
            document.body.removeChild(popup);
        });

        document.getElementById('cancel-rule').addEventListener('click', () => {
            document.body.removeChild(popup);
        });

        function convertSimpleToRegex(logic, text) {
            const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            switch (logic) {
                case 'contains': return `.*${escapedText}.*`;
                case 'not-contains': return `^(?!.*${escapedText}).*$`;
                case 'equals': return `^${escapedText}$`;
                default: return escapedText;
            }
        }

        function attachPreviewListeners() {
            const previewBtn = inputContainer.querySelector('.preview-rule');
            let previewActive = false;
            let affectedElements = [];

            previewBtn.addEventListener('click', () => {
                if (!previewActive) {
                    let regex, level;
                    if (isSimpleMode) {
                        const logic = inputContainer.querySelector('.logic-select').value;
                        const text = inputContainer.querySelector('.simple-input').value.trim();
                        level = parseInt(inputContainer.querySelector('.level-input').value, 10);
                        if (!text || isNaN(level) || level < 0) {
                            alert('请输入有效的文本和层级');
                            return;
                        }
                        regex = convertSimpleToRegex(logic, text);
                    } else {
                        regex = inputContainer.querySelector('.regex-input').value.trim();
                        level = parseInt(inputContainer.querySelector('.level-input').value, 10);
                        if (!regex || isNaN(level) || level < 0) {
                            alert('请输入有效的正则规则和层级');
                            return;
                        }
                    }
                    try {
                        const ruleRegex = new RegExp(regex);
                        const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
                            acceptNode: (node) => {
                                const parent = node.parentElement;
                                return (parent && (parent.closest('.blocker-popup') || parent.closest('.blocker-list')))
                                    ? NodeFilter.FILTER_REJECT
                                    : NodeFilter.FILTER_ACCEPT;
                            }
                        }, false);
                        let node;
                        affectedElements = [];
                        while (node = walker.nextNode()) {
                            if (ruleRegex.test(node.textContent)) {
                                let element = node.parentElement;
                                for (let i = 0; i < level; i++) {
                                    if (element.parentElement) element = element.parentElement;
                                    else break;
                                }
                                affectedElements.push(element);
                                element.style.display = 'none';
                            }
                        }
                        previewActive = true;
                        previewBtn.textContent = '取消预览';
                    } catch (e) {
                        alert('正则表达式无效,请检查输入');
                    }
                } else {
                    affectedElements.forEach(el => el.style.display = '');
                    affectedElements = [];
                    previewActive = false;
                    previewBtn.textContent = '预览';
                }
            });
        }

        renderRules();
    }

    // 清除当前域名屏蔽规则
    function clearDomainBlocks() {
        const domain = window.location.hostname;
        if (window.confirm(`是否确认清除当前域名 (${domain}) 下的所有屏蔽规则?`)) {
            let blocks = GM_getValue('blocks', {});
            let dynamicBlocks = GM_getValue('dynamicBlocks', {});
            let regexBlocks = GM_getValue('regexBlocks', {});

            delete blocks[domain];
            delete dynamicBlocks[domain];
            delete regexBlocks[domain];

            GM_setValue('blocks', blocks);
            GM_setValue('dynamicBlocks', dynamicBlocks);
            GM_setValue('regexBlocks', regexBlocks);

            // 刷新页面以恢复显示
            window.location.reload();
        }
    }

    // 页面加载时立即应用所有屏蔽规则
    applyBlocks();
    const observer = new MutationObserver(() => applyBlocks());
    observer.observe(document.body, { childList: true, subtree: true });
})();