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