1const CosmeticFilter = (() => {
2 const hiddenElements = new WeakSet();
3 const selectors = {
4 generic: [], // Apply everywhere
5 specific: [], // Domain-specific
6 procedural: [] // Complex selectors with :has(), etc.
7 };
8
9 // Safe anti-breakage scriptlets that avoid WebKit internal interference
10 const antiBreakage = {
11 // Safely stub ad services without modifying global setTimeout
12 neutralizeAdPromises() {
13 try {
14 // Use WeakMap to avoid direct property modification
15 const stubServices = new WeakMap();
16
17 // Only stub if not already defined to avoid conflicts
18 if (typeof window.googletag === 'undefined') {
19 window.googletag = {
20 cmd: [],
21 push: () => {},
22 enableServices: () => {},
23 display: () => {},
24 defineSlot: () => ({ addService: () => {}, setTargeting: () => {} })
25 };
26 }
27
28 // Stub Google Analytics safely
29 if (typeof window.gtag === 'undefined') {
30 window.gtag = function() {};
31 }
32 if (typeof window.ga === 'undefined') {
33 window.ga = function() {};
34 }
35 } catch (e) {
36 // Fail silently to avoid breaking pages
37 console.debug('[dumber] Anti-breakage initialization failed:', e);
38 }
39 },
40
41 // Clean up loading indicators after page load
42 cleanupLoadingIndicators() {
43 // Wait for page to be more stable before cleanup
44 const cleanup = () => {
45 try {
46 // Only hide elements that are clearly loading indicators
47 const loadingSelectors = [
48 '.loading:not([role])', // Avoid hiding important loading states
49 '.spinner:not([aria-live])',
50 '.loader:not([aria-live])',
51 '[class*="ad-loading"]:not([aria-live])'
52 ];
53
54 loadingSelectors.forEach(selector => {
55 document.querySelectorAll(selector).forEach(el => {
56 // Check if element is actually a loading indicator
57 const rect = el.getBoundingClientRect();
58 if (rect.width > 0 && rect.height > 0) {
59 el.style.opacity = '0.3';
60 el.style.transition = 'opacity 0.5s';
61 }
62 });
63 });
64 } catch (e) {
65 console.debug('[dumber] Loading cleanup failed:', e);
66 }
67 };
68
69 // Defer cleanup to avoid interfering with page initialization
70 if (document.readyState === 'complete') {
71 setTimeout(cleanup, 2000);
72 } else {
73 window.addEventListener('load', () => setTimeout(cleanup, 2000));
74 }
75 }
76 };
77
78 // High-performance element hiding
79 const hideElements = (elements) => {
80 const styleId = 'dumber-cosmetic-style';
81 let styleEl = document.getElementById(styleId);
82
83 if (!styleEl) {
84 styleEl = document.createElement('style');
85 styleEl.id = styleId;
86 styleEl.textContent = '';
87 (document.head || document.documentElement).appendChild(styleEl);
88 }
89
90 // Batch CSS injection for performance
91 const cssRules = elements.map(selector =>
92 `${selector} { display: none !important; }`
93 ).join('\n');
94
95 styleEl.textContent += cssRules;
96 };
97
98 // Handle complex procedural selectors
99 const applyProceduralFilters = () => {
100 selectors.procedural.forEach(rule => {
101 if (rule.includes(':has(')) {
102 // Handle :has() pseudo-selector
103 const match = rule.match(/(.+):has\((.+)\)/);
104 if (match) {
105 const [_, parent, child] = match;
106 document.querySelectorAll(parent).forEach(el => {
107 if (el.querySelector(child)) {
108 el.style.display = 'none';
109 hiddenElements.add(el);
110 }
111 });
112 }
113 } else if (rule.includes(':not(')) {
114 // Handle :not() pseudo-selector
115 try {
116 document.querySelectorAll(rule).forEach(el => {
117 el.style.display = 'none';
118 hiddenElements.add(el);
119 });
120 } catch (e) {
121 // Invalid selector, skip
122 }
123 }
124 });
125 };
126
127 // MutationObserver for lazy-loaded content
128 const observer = new MutationObserver((mutations) => {
129 // Debounce for performance
130 if (observer.timeout) {
131 clearTimeout(observer.timeout);
132 }
133
134 observer.timeout = setTimeout(() => {
135 // Check if new elements match our selectors
136 const allSelectors = [...selectors.generic, ...selectors.specific];
137
138 mutations.forEach(mutation => {
139 // Handle added nodes
140 mutation.addedNodes.forEach(node => {
141 if (node.nodeType !== 1) return; // Only element nodes
142
143 // Check the node itself
144 allSelectors.forEach(selector => {
145 if (node.matches && node.matches(selector)) {
146 node.style.display = 'none';
147 hiddenElements.add(node);
148 }
149 });
150
151 // Check descendants
152 if (node.querySelectorAll) {
153 allSelectors.forEach(selector => {
154 node.querySelectorAll(selector).forEach(el => {
155 el.style.display = 'none';
156 hiddenElements.add(el);
157 });
158 });
159 }
160 });
161
162 // Handle attribute changes (for dynamic ads)
163 if (mutation.type === 'attributes') {
164 const target = mutation.target;
165 allSelectors.forEach(selector => {
166 if (target.matches && target.matches(selector)) {
167 target.style.display = 'none';
168 hiddenElements.add(target);
169 }
170 });
171 }
172 });
173
174 // Reapply procedural filters
175 applyProceduralFilters();
176 }, 50); // 50ms debounce
177 });
178
179 // Initialize cosmetic filtering
180 const init = (rules) => {
181 // Initialize safe anti-breakage scriptlets
182 antiBreakage.neutralizeAdPromises();
183 antiBreakage.cleanupLoadingIndicators();
184
185 // Parse and categorize rules
186 rules.forEach(rule => {
187 if (rule.includes(':has(') || rule.includes(':not(')) {
188 selectors.procedural.push(rule);
189 } else if (rule.domain === window.location.hostname) {
190 selectors.specific.push(rule.selector);
191 } else {
192 selectors.generic.push(rule.selector);
193 }
194 });
195
196 // Apply initial hiding
197 hideElements([...selectors.generic, ...selectors.specific]);
198 applyProceduralFilters();
199
200 // Start observing for changes
201 observer.observe(document.documentElement, {
202 childList: true,
203 subtree: true,
204 attributes: true,
205 attributeFilter: ['class', 'id', 'style']
206 });
207 };
208
209 // Performance optimization: batch rule updates
210 const updateRules = (newRules) => {
211 // Diff and apply only new rules
212 const newSelectors = newRules.filter(r =>
213 !selectors.generic.includes(r) &&
214 !selectors.specific.includes(r)
215 );
216
217 if (newSelectors.length > 0) {
218 hideElements(newSelectors);
219 selectors.generic.push(...newSelectors);
220 }
221 };
222
223 // Cleanup function for navigation
224 const cleanup = () => {
225 observer.disconnect();
226 hiddenElements.clear();
227 const styleEl = document.getElementById('dumber-cosmetic-style');
228 if (styleEl) styleEl.remove();
229 };
230
231 return { init, updateRules, cleanup };
232})();
233
234// Inject rules from native
235window.__dumber_cosmetic_init = (rules) => CosmeticFilter.init(rules);
236window.__dumber_cosmetic_update = (rules) => CosmeticFilter.updateRules(rules);
237window.__dumber_cosmetic_cleanup = () => CosmeticFilter.cleanup();