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