// ==UserScript== // @name 劈里啪啦小黄脸屏蔽 // @version 1.0 // @author shanbei2033 // @description Hide Bilibili comment-area yellow-face emotes, including avif-rendered ones. // @match *://*.bilibili.com/* // @run-at document-start // @grant none // ==/UserScript== (() => { 'use strict'; const STYLE_ID = 'pw-hide-bilibili-comment-emotes'; const WATCHED_ROOTS = new WeakSet(); const BRACKET_ALT_PATTERN = /^\[[^[\]]+\]$/; const COMMENT_HINT_PATTERN = /(comment|reply|root-reply|sub-reply)/i; const CSS_TEXT = ` img[src*="/bfs/emote/"][alt^="["][alt$="]"], img[data-src*="/bfs/emote/"][alt^="["][alt$="]"] { display: none !important; width: 0 !important; height: 0 !important; margin: 0 !important; padding: 0 !important; } picture:has(img[src*="/bfs/emote/"][alt^="["][alt$="]"]), picture:has(img[data-src*="/bfs/emote/"][alt^="["][alt$="]"]) { display: none !important; } `; function normalizeText(value) { return typeof value === 'string' ? value : ''; } function normalizeStyle(styleText) { return normalizeText(styleText).replace(/\s+/g, '').toLowerCase(); } function getSourceText(img) { const values = [ img.getAttribute('src'), img.getAttribute('data-src'), img.getAttribute('srcset'), img.currentSrc, ]; const picture = img.closest('picture'); if (picture) { picture.querySelectorAll('source').forEach((source) => { values.push(source.getAttribute('srcset')); values.push(source.getAttribute('data-srcset')); }); } return values.filter(Boolean).join(' '); } function hasYellowFaceSource(img) { return getSourceText(img).includes('/bfs/emote/'); } function hasYellowFaceStyle(img) { const inlineStyle = normalizeStyle(img.getAttribute('style')); if ( inlineStyle.includes('width:1.4em') || inlineStyle.includes('height:1.4em') || inlineStyle.includes('vertical-align:text-bottom') ) { return true; } if (!img.isConnected) { return false; } const computed = getComputedStyle(img); const width = parseFloat(computed.width); const height = parseFloat(computed.height); return ( computed.verticalAlign === 'text-bottom' && Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0 && width <= 40 && height <= 40 ); } function isInsideCommentTree(node) { let current = node; while (current) { if (current instanceof ShadowRoot) { current = current.host; continue; } if (current instanceof Element) { const hint = [ current.tagName, current.id, normalizeText(current.getAttribute('class')), normalizeText(current.getAttribute('data-testid')), ].join(' '); if (COMMENT_HINT_PATTERN.test(hint)) { return true; } } current = current.parentNode; } return false; } function isYellowFaceEmote(img) { if (!(img instanceof HTMLImageElement)) { return false; } const alt = normalizeText(img.getAttribute('alt')).trim(); if (!BRACKET_ALT_PATTERN.test(alt)) { return false; } if (!hasYellowFaceSource(img)) { return false; } return hasYellowFaceStyle(img) || isInsideCommentTree(img); } function hideImage(img) { if (!isYellowFaceEmote(img) || img.dataset.pwEmoteBlocked === '1') { return; } img.dataset.pwEmoteBlocked = '1'; img.style.setProperty('display', 'none', 'important'); img.style.setProperty('width', '0', 'important'); img.style.setProperty('height', '0', 'important'); img.style.setProperty('margin', '0', 'important'); img.style.setProperty('padding', '0', 'important'); img.style.setProperty('min-width', '0', 'important'); img.style.setProperty('min-height', '0', 'important'); const picture = img.closest('picture'); if (picture) { picture.style.setProperty('display', 'none', 'important'); picture.style.setProperty('width', '0', 'important'); picture.style.setProperty('height', '0', 'important'); } } function processNode(node) { if (!(node instanceof Element || node instanceof ShadowRoot || node instanceof Document)) { return; } if (node instanceof HTMLImageElement) { hideImage(node); } else { node.querySelectorAll('img').forEach(hideImage); } if (node instanceof Element && node.shadowRoot) { watchRoot(node.shadowRoot); } if ('querySelectorAll' in node) { node.querySelectorAll('*').forEach((element) => { if (element.shadowRoot) { watchRoot(element.shadowRoot); } }); } } function processAttributeTarget(node) { if (node instanceof HTMLImageElement) { hideImage(node); return; } if (node instanceof HTMLSourceElement) { const picture = node.closest('picture'); const img = picture ? picture.querySelector('img') : null; if (img) { hideImage(img); } } } function injectStyle(root) { if (!root || !('querySelector' in root) || root.querySelector(`#${STYLE_ID}`)) { return; } const ownerDocument = root instanceof Document ? root : root.ownerDocument || document; const style = ownerDocument.createElement('style'); style.id = STYLE_ID; style.textContent = CSS_TEXT; if (root instanceof Document) { const parent = root.head || root.documentElement; if (parent) { parent.appendChild(style); } return; } root.appendChild(style); } function watchRoot(root) { if (!root || WATCHED_ROOTS.has(root)) { return; } WATCHED_ROOTS.add(root); injectStyle(root); processNode(root); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'attributes') { processAttributeTarget(mutation.target); continue; } mutation.addedNodes.forEach(processNode); } }); observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'data-src', 'srcset', 'data-srcset', 'alt', 'style'], }); } const nativeAttachShadow = Element.prototype.attachShadow; Element.prototype.attachShadow = function attachShadowPatched(init) { const shadowRoot = nativeAttachShadow.call(this, init); watchRoot(shadowRoot); return shadowRoot; }; function start() { watchRoot(document); processNode(document); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start, { once: true }); } else { start(); } })();