Skip to content

Instantly share code, notes, and snippets.

@hui1601
Last active April 7, 2025 01:23
Show Gist options
  • Save hui1601/ba69b63749954e3aed7510b091cd5370 to your computer and use it in GitHub Desktop.
Save hui1601/ba69b63749954e3aed7510b091cd5370 to your computer and use it in GitHub Desktop.
namupower.user.js
// ==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