Last active
April 7, 2025 01:23
-
-
Save hui1601/ba69b63749954e3aed7510b091cd5370 to your computer and use it in GitHub Desktop.
namupower.user.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name NamuPower | |
// @namespace Violentmonkey Scripts | |
// @match https://namu.wiki/w/* | |
// @grant none | |
// @version 1.7 | |
// @author - | |
// @description Removes advertisements from namu.wiki pages | |
// @run-at document-start | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
// Configuration constants | |
const CONFIG = { | |
REMOVAL_DELAY: 10, // ms delay before removing ad elements | |
AD_SVG_DIMENSIONS: [ | |
{ width: 50, height: 15 }, | |
{ width: 30, height: 16 } | |
], | |
AD_TEXT_MARKERS: ['파워링크', '광고'] | |
}; | |
/** | |
* Ad detection and removal service | |
*/ | |
const AdRemover = { | |
// Store identified ad classes | |
knownAdClasses: new Set(), | |
/** | |
* Initialize the ad removal observer | |
*/ | |
initialize() { | |
const observer = new MutationObserver(this.handleDomMutations.bind(this)); | |
observer.observe(document, { subtree: true, childList: true }); | |
console.log('NamuPower: Ad blocker initialized'); | |
}, | |
/** | |
* Process DOM mutations to detect and remove ads | |
*/ | |
handleDomMutations(mutations) { | |
mutations.forEach(mutation => { | |
if (!mutation.addedNodes.length) return; | |
Array.from(mutation.addedNodes) | |
.filter(node => node instanceof HTMLElement) | |
.forEach(node => this.processNode(node)); | |
}); | |
}, | |
/** | |
* Process a single node to check if it's an ad | |
*/ | |
processNode(node) { | |
const parent = node.parentElement; | |
if (!parent) return; | |
if (this.isTableBasedAd(node, parent)) { | |
this.removeTableAd(node, parent); | |
} | |
else if (this.isDivBasedAd(node, parent)) { | |
this.removeDivAd(node, parent); | |
} | |
else if (this.isMobileAd(node, parent)) { | |
this.removeMobileAd(node, parent); | |
} | |
}, | |
/** | |
* Check if node is a table-based advertisement | |
*/ | |
isTableBasedAd(node, parent) { | |
if (parent.tagName !== 'DIV' || node.tagName !== 'TABLE') return false; | |
const svgs = this.findSvgImages(node); | |
return svgs.length > 0 && this.containsAdSvgPattern(svgs); | |
}, | |
/** | |
* Remove a table-based advertisement | |
*/ | |
removeTableAd(node, parent) { | |
setTimeout(() => { | |
if (parent.parentElement) { | |
this.hideOffscreen(parent.parentElement); | |
} | |
parent.remove(); | |
}, CONFIG.REMOVAL_DELAY); | |
}, | |
/** | |
* Check if node is a div-based advertisement | |
*/ | |
isDivBasedAd(node, parent) { | |
if (parent.tagName !== 'DIV' || parent.className !== '' || | |
!parent.parentElement || parent.parentElement.tagName !== 'DIV') { | |
return false; | |
} | |
const parentDiv = parent.parentElement; | |
// Check for ad indicators or SVG patterns | |
return this.hasAdIndicators(parentDiv) || | |
this.containsAdSvgPattern(this.findSvgImages(parentDiv)); | |
}, | |
/** | |
* Check if the element contains ad indicators | |
*/ | |
hasAdIndicators(element) { | |
const spans = Array.from(element.querySelectorAll('span')); | |
// Check for background image indicators | |
const hasBackgroundImages = spans.filter(span => | |
window.getComputedStyle(span).backgroundImage.startsWith('url("data:image/png;base64,') | |
).length >= 2; | |
// Check for ad text markers | |
const hasAdText = CONFIG.AD_TEXT_MARKERS.some(marker => | |
spans.some(span => span.innerText === marker) | |
); | |
return hasBackgroundImages || hasAdText; | |
}, | |
/** | |
* Remove a div-based advertisement | |
*/ | |
removeDivAd(node, parent) { | |
const parentDiv = parent.parentElement; | |
// If it has ad indicators, just remove it directly | |
if (this.hasAdIndicators(parentDiv)) { | |
parentDiv.remove(); | |
return; | |
} | |
// Handle SVG-based ads | |
setTimeout(() => { | |
const container = parentDiv?.parentElement?.parentElement; | |
if (container) { | |
this.hideOffscreen(container); | |
const targetElement = parentDiv?.parentElement; | |
if (targetElement) { | |
if (targetElement.className) { | |
this.knownAdClasses.add(targetElement.className); | |
} | |
targetElement.remove(); | |
} | |
} | |
}, CONFIG.REMOVAL_DELAY); | |
}, | |
/** | |
* Check if node is a mobile advertisement | |
*/ | |
isMobileAd(node, parent) { | |
if (node.tagName !== 'SPAN' || parent.tagName !== 'SPAN' || | |
!parent.parentElement || parent.parentElement.tagName !== 'DIV') { | |
return false; | |
} | |
// Check for images in the node | |
if (!node.querySelectorAll('img').length) return false; | |
const parentDiv = parent.parentElement; | |
const svgs = this.findSvgImages(parentDiv); | |
// Check for SVG patterns or color styled images | |
return (svgs.length > 0 && this.containsAdSvgPattern(svgs)) || | |
this.hasColoredImages(node); | |
}, | |
/** | |
* Check if node has images with color styles | |
*/ | |
hasColoredImages(node) { | |
return Array.from(node.querySelectorAll('img')) | |
.some(img => img.style.color !== ''); | |
}, | |
/** | |
* Remove a mobile advertisement | |
*/ | |
removeMobileAd(node, parent) { | |
const parentDiv = parent.parentElement; | |
const svgs = this.findSvgImages(parentDiv); | |
// SVG-based mobile ads | |
if (svgs.length > 0 && this.containsAdSvgPattern(svgs)) { | |
setTimeout(() => { | |
const container = parentDiv?.parentElement; | |
if (container) { | |
container.style.display = 'none'; | |
} | |
}, CONFIG.REMOVAL_DELAY); | |
return; | |
} | |
// Colored image mobile ads | |
if (!this.hasColoredImages(node)) return; | |
const grandParent = parentDiv?.parentElement; | |
const container = grandParent?.parentElement; | |
if (grandParent && container) { | |
container.style.minHeight = '0'; | |
container.style.height = '0'; | |
if (container.className) { | |
this.knownAdClasses.add(container.className); | |
} | |
grandParent.parent.remove(); | |
} | |
}, | |
/** | |
* Find all SVG images in an element | |
*/ | |
findSvgImages(element) { | |
return Array.from(element.querySelectorAll('img')) | |
.filter(img => img.src?.trim().startsWith('data:image/svg+xml;base64,')); | |
}, | |
/** | |
* Hide an element by moving it off-screen | |
*/ | |
hideOffscreen(element) { | |
if (!element) return; | |
element.style.position = 'absolute'; | |
element.style.top = '-9999px'; | |
element.style.left = '-9999px'; | |
}, | |
/** | |
* Checks if SVGs match known ad patterns | |
*/ | |
containsAdSvgPattern(svgElements) { | |
for (const svg of svgElements) { | |
try { | |
const base64Content = svg.src.split('base64,')[1]; | |
if (!base64Content) continue; | |
const svgContent = atob(base64Content); | |
const doc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); | |
const svgElement = doc.querySelector('svg'); | |
if (!this.isValidAdSvg(svgElement)) continue; | |
const width = parseInt(svgElement.getAttribute('width')); | |
const height = parseInt(svgElement.getAttribute('height')); | |
if (this.matchesAdDimensions(width, height)) { | |
return true; | |
} | |
} catch (error) { | |
// Silent fail for parsing errors | |
} | |
} | |
return false; | |
}, | |
/** | |
* Validates an SVG element for ad criteria | |
*/ | |
isValidAdSvg(svg) { | |
return svg && | |
svg.hasAttribute('width') && | |
svg.hasAttribute('height') && | |
svg.getAttribute('xmlns') === 'http://www.w3.org/2000/svg' && | |
svg.children.length === 0; | |
}, | |
/** | |
* Checks if dimensions match known ad dimensions | |
*/ | |
matchesAdDimensions(width, height) { | |
if (isNaN(width) || isNaN(height)) return false; | |
return CONFIG.AD_SVG_DIMENSIONS.some(dimensions => | |
dimensions.width === width && dimensions.height === height | |
); | |
} | |
}; | |
// Initialize the ad remover | |
AdRemover.initialize(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment