-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
374 lines (333 loc) · 13.8 KB
/
Copy pathcontent.js
File metadata and controls
374 lines (333 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
// deslop — content script
// Hides AI Overview blocks, the AI Mode tab in the SERP nav, and acts as a
// DOM-level fallback for escaping AI Mode pages (secondary to the URL-strip
// in background.js / rules-ai.json).
//
// Safety priority over coverage: we'd rather leave a stray AI Overview block
// visible than ever blank out the user's results page. So all hide logic
// goes through guards that abort if the candidate container is too tall
// or is one of Google's main page wrappers.
(function () {
const STYLE_ID = "deslop-injected-styles";
const SCANNER_FLAG = "_deslop_observer";
const HIDDEN_FLAG = "deslopHidden";
const REDIRECT_KEY = "_deslop_last_escape_at";
const REDIRECT_COOLDOWN_MS = 3000;
// Containers we must NEVER hide. These are Google's top-level wrappers
// for the whole results page; hiding them blanks the SERP.
const NEVER_HIDE_IDS = new Set([
"search", // top-level result region
"rso", // result-set organic (the list of results)
"rcnt", // results container
"center_col", // main center column
"cnt", // outer content wrapper
"main", // <main> tag
"appbar", // top bar
"searchform", // search box wrapper
"tsf", // search form
"result-stats", // result count strip
"hdtb", // tools bar
"extabar", // expanded tools area
"before-appbar",
"topabar",
]);
// ---------- CSS (tight — exact attribute matches only) ----------
// AI Overview signatures that are precise enough not to overmatch:
// * #m-x-content — Google's current ID for the AI Overview body container.
// Unique per page, so :has(#m-x-content) safely identifies the parent
// result-block (data-hveid wrapper) holding the entire AI Overview module.
// * exact data-attrid values that mention AIOverview (older signature)
// * exact aria-label "AI Overview" on a small element (older signature)
// No substring matches (`*=`) on data-async-context or data-subtree —
// those leak to parent containers.
const AI_OVERVIEW_CSS = `
#m-x-content,
div[data-hveid]:has(#m-x-content),
div[data-attrid="AIOverview"],
div[data-attrid="ai_overview"],
div[aria-label="AI Overview"],
div[aria-label="AI overview"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
`;
// AI Mode entry points come in two forms:
// * The SERP top-nav "AI Mode" tab (a link with href containing udm=50)
// * The pill button inside the homepage search input (no href, just a
// <button> with Google's current jsname for the AI Mode trigger)
// Selectors target the link/tab/button specifically and cannot
// accidentally hide the whole results region.
const AI_MODE_TAB_CSS = `
a[href*="udm=50"],
a[aria-label*="AI Mode" i],
button[aria-label*="AI Mode" i],
div[role="listitem"]:has(> a[href*="udm=50"]),
div[role="listitem"]:has(> a[aria-label*="AI Mode" i]),
div[role="tab"]:has(> a[href*="udm=50"]),
li:has(> a[href*="udm=50"]),
button[jsname="B6rgad"] {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
`;
function ensureStyle() {
let s = document.getElementById(STYLE_ID);
if (!s) {
s = document.createElement("style");
s.id = STYLE_ID;
(document.head || document.documentElement).appendChild(s);
}
return s;
}
function setCSS(css) {
if (!css) {
const s = document.getElementById(STYLE_ID);
if (s) s.remove();
return;
}
ensureStyle().textContent = css;
}
function buildCSS(settings) {
const parts = [];
if (settings.hideOverview !== false) parts.push(AI_OVERVIEW_CSS);
if (settings.hideAIModeTab !== false) parts.push(AI_MODE_TAB_CSS);
return parts.join("\n");
}
// ---------- AI Overview text-content scanner (with strong guards) -------
// Localized AI Overview heading patterns. Google translates the label per
// locale, so a single English match misses Hungarian "AI-alapú áttekintés",
// German "KI-Übersicht", French "Aperçu IA", etc. Each pattern is anchored
// tight enough to avoid matching organic results that happen to mention
// AI.
const AI_OVERVIEW_PATTERNS = [
/^\s*ai\s+overview\b/i, // en: AI Overview
/^\s*ai[-\s]?alap/i, // hu: AI-alapú áttekintés
/^\s*ki[-\s]?übersicht/i, // de: KI-Übersicht
/\baperçu\s+ia\b/i, // fr: Aperçu IA
/\bvista\s+general\s+(?:de\s+)?ia\b/i, // es: Vista general de IA
/\bpanoramica\s+ia\b/i, // it: Panoramica IA
/^\s*ai[-\s]?overzicht/i, // nl: AI-overzicht
/\bprzegląd\s+ai\b/i, // pl: Przegląd AI
/^\s*ai\s*概要/, // ja: AI概要
/\bcoup\s*d['']?œil\s*ia\b/i, // fr (Canada) alt phrasing
/^\s*resumen\s+(?:con\s+)?ia\b/i, // es alt
];
function isAIOverviewHeading(el) {
const label = (el.getAttribute("aria-label") || "").trim();
const text = ((el.innerText || el.textContent) || "").trim();
for (const p of AI_OVERVIEW_PATTERNS) {
if (label && p.test(label)) return true;
if (text && p.test(text)) return true;
}
return false;
}
// Upper bound on what we're willing to hide. The AI Overview block is at
// most a few hundred pixels; the SERP wrapper is 2000-4000px. 1500px is a
// comfortable ceiling that lets us hide a tall AI Overview on a small
// viewport (where viewport*0.65 alone would be too tight) without ever
// hiding the whole results region.
function safeHeightCeiling() {
return Math.max(1500, window.innerHeight * 0.65);
}
function isSafeToHide(node) {
if (!node || node === document.body || node === document.documentElement) return false;
if (node.id && NEVER_HIDE_IDS.has(node.id)) return false;
// role="main" anywhere we hit is also a hard stop
if (node.getAttribute && node.getAttribute("role") === "main") return false;
// Never hide anything that's clearly the whole SERP region.
const rect = node.getBoundingClientRect();
if (rect.height > safeHeightCeiling()) return false;
// Don't hide essentially-empty wrappers either.
if (rect.height < 20) return false;
return true;
}
// Walk up from the heading looking for the AI Overview wrapper. We pass
// through 0-height structural wrappers (Google uses `display:contents`
// divs as layout glue between the heading and the real block), and stop
// on the first ancestor that's either an explicit AI Overview marker, a
// data-hveid result-block boundary, or a hard guard.
function findOverviewContainer(heading) {
let node = heading;
let bestSafe = null;
for (let i = 0; i < 12; i++) {
const parent = node.parentElement;
if (!parent) break;
if (parent.id && NEVER_HIDE_IDS.has(parent.id)) break;
if (parent.getAttribute && parent.getAttribute("role") === "main") break;
const rect = parent.getBoundingClientRect();
// Hard ceiling: bail before climbing into the whole-SERP wrapper.
if (rect.height > safeHeightCeiling()) break;
// Pass-through: 0-height `display:contents`-style wrappers carry no
// visual content of their own. Keep walking up instead of breaking,
// since the real AI Overview block sits a few levels above them.
if (rect.height < 20) {
node = parent;
continue;
}
// Prefer an explicit AI Overview signature on the ancestor itself.
const attrid = parent.getAttribute("data-attrid") || "";
if (/^ai[_-]?overview$/i.test(attrid) || /^aioverview$/i.test(attrid)) {
return parent;
}
// Stop at the first data-hveid ancestor — that's Google's natural
// result-block boundary. Hiding it removes the whole AI Overview
// module cleanly without leaking into adjacent organic results
// (each of which has its own data-hveid wrapper).
if (parent.hasAttribute("data-hveid")) return parent;
bestSafe = parent;
node = parent;
}
return bestSafe; // may be null
}
function scanAndHideOverview() {
const candidates = document.querySelectorAll(
'h1, h2, h3, [role="heading"], [aria-label^="AI Overview" i]'
);
for (const el of candidates) {
if (!isAIOverviewHeading(el)) continue;
const container = findOverviewContainer(el);
if (!container) continue;
if (container.dataset && container.dataset[HIDDEN_FLAG] === "1") continue;
if (container.dataset) container.dataset[HIDDEN_FLAG] = "1";
container.style.setProperty("display", "none", "important");
}
}
// ---------- AI Mode entry-button text scanner (fallback) ----------
// The homepage AI Mode pill currently has jsname="B6rgad", caught by the
// CSS above. When Google rotates that jsname (they do, periodically), we
// fall back to matching the button's visible text against localized
// "AI Mode" labels. Patterns are anchored ^...$ to the full text so they
// only fire on the literal pill, never on organic content that mentions
// AI mode in passing.
const AI_MODE_BUTTON_PATTERNS = [
/^\s*ai[\s-]?mode\s*$/i, // en: AI Mode
/^\s*ai[-\s]?mód\s*$/i, // hu: AI-mód
/^\s*ki[-\s]?modus\s*$/i, // de: KI-Modus
/^\s*mode\s+ia\s*$/i, // fr: Mode IA
/^\s*modo\s+ia\s*$/i, // es / pt: Modo IA
/^\s*modalità\s+ia\s*$/i, // it: Modalità IA
/^\s*ai[-\s]?modus\s*$/i, // nl: AI-modus
/^\s*tryb\s+ai\s*$/i, // pl: Tryb AI
/^\s*ai\s*モード\s*$/, // ja: AIモード
];
function scanAndHideAIModeButton() {
const buttons = document.querySelectorAll(
'button, a[role="button"], div[role="button"]'
);
for (const btn of buttons) {
const text = ((btn.innerText || btn.textContent) || "").trim();
if (!text || text.length > 30) continue;
if (!AI_MODE_BUTTON_PATTERNS.some((p) => p.test(text))) continue;
// Safety: small pill-sized elements only — never hide huge buttons.
const rect = btn.getBoundingClientRect();
if (rect.width > 250 || rect.height > 60) continue;
if (btn.dataset && btn.dataset[HIDDEN_FLAG] === "1") continue;
if (btn.dataset) btn.dataset[HIDDEN_FLAG] = "1";
btn.style.setProperty("display", "none", "important");
}
}
// ---------- AI Mode page detection (secondary; URL strip is primary) ----
function isOnAIModePage() {
if (document.querySelector(
'a[href*="udm=50"][aria-current="page"],' +
'a[href*="udm=50"][aria-selected="true"],' +
'a[aria-label*="AI Mode" i][aria-current="page"],' +
'a[aria-label*="AI Mode" i][aria-selected="true"]'
)) return true;
const h = document.querySelector('h1, [role="heading"][aria-level="1"]');
if (h && /^\s*ai mode\s*$/i.test(h.textContent || "")) return true;
return false;
}
function findAllTabHref() {
const links = document.querySelectorAll(
'div[role="navigation"] a[href*="/search"],' +
'[role="list"] a[href*="/search"],' +
'nav a[href*="/search"]'
);
for (const link of links) {
let u;
try { u = new URL(link.href, window.location.origin); }
catch (_) { continue; }
if (u.pathname !== "/search") continue;
if (u.searchParams.has("udm")) continue;
if (u.searchParams.has("tbm")) continue;
return u.toString();
}
return null;
}
function tryEscapeAIMode() {
let last = 0;
try { last = parseInt(sessionStorage.getItem(REDIRECT_KEY) || "0", 10); }
catch (_) {}
if (Date.now() - last < REDIRECT_COOLDOWN_MS) return;
if (!isOnAIModePage()) return;
let target = findAllTabHref();
if (!target) {
const u = new URL(window.location.href);
if (u.searchParams.has("udm")) {
u.searchParams.delete("udm");
target = u.toString();
}
}
if (!target || target === window.location.href) return;
try { sessionStorage.setItem(REDIRECT_KEY, String(Date.now())); }
catch (_) {}
window.location.replace(target);
}
// ---------- Scanner / observer ----------
function startScanner(settings) {
if (window[SCANNER_FLAG]) {
window[SCANNER_FLAG]._settings = settings;
runScans(settings);
return;
}
const observer = new MutationObserver(() => {
if (observer._scheduled) return;
observer._scheduled = setTimeout(() => {
observer._scheduled = null;
try { runScans(observer._settings); } catch (_) {}
}, 50);
});
observer._settings = settings;
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
window[SCANNER_FLAG] = observer;
runScans(settings);
}
function runScans(settings) {
if (!settings) return;
if (settings.hideOverview !== false) scanAndHideOverview();
if (settings.hideAIModeTab !== false) scanAndHideAIModeButton();
if (settings.skipAIMode !== false) tryEscapeAIMode();
}
function stopScanner() {
const o = window[SCANNER_FLAG];
if (o) {
o.disconnect();
delete window[SCANNER_FLAG];
}
}
// ---------- Apply settings ----------
function apply(settings) {
setCSS(buildCSS(settings));
const needScanner =
settings.hideOverview !== false ||
settings.hideAIModeTab !== false ||
settings.skipAIMode !== false;
if (needScanner) startScanner(settings);
else stopScanner();
}
const STORAGE_KEYS = ["hideOverview", "hideAIModeTab", "skipAIMode"];
chrome.storage.sync.get(STORAGE_KEYS, (s) => apply(s || {}));
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== "sync") return;
if (!STORAGE_KEYS.some((k) => k in changes)) return;
chrome.storage.sync.get(STORAGE_KEYS, (s) => apply(s || {}));
});
})();