detached 27d0775

fix(cosmetic): inject rules immediately and fix selector normalization

commit
27d0775c8f34eb4ac9b9bfdc59f863abb9526f57
author
bnema · · 2025-11-29 11:26 UTC
parent
6defb29
signature
GPG Unverified2B82207D370127A0 The key is unknown
3 files · +83 -26
M internal/filtering/cosmetic/injector.go +23 -8
@@ -67,7 +67,7 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
6767 return ""
6868 }
6969
70- // Build rules for this specific domain
70+ // Build rules for this specific domain (including parent domains)
7171 var rules []CosmeticRule
7272
7373 // Add generic rules
@@ -77,16 +77,31 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
7777 })
7878 }
7979
80- // Add domain-specific rules
81- if domainSelectors, exists := ci.domainRules[domain]; exists {
82- for _, selector := range domainSelectors {
83- rules = append(rules, CosmeticRule{
84- Domain: domain,
85- Selector: selector,
86- })
80+ // Add domain-specific rules for the domain and its parent domains
81+ addDomainRules := func(domainKey string) {
82+ if domainKey == "" {
83+ return
84+ }
85+ if domainSelectors, exists := ci.domainRules[domainKey]; exists {
86+ for _, selector := range domainSelectors {
87+ rules = append(rules, CosmeticRule{
88+ Domain: domainKey,
89+ Selector: selector,
90+ })
91+ }
8792 }
8893 }
8994
95+ addDomainRules(domain)
96+
97+ // Walk up parent domains to include base-domain rules for subdomains
98+ parts := strings.Split(domain, ".")
99+ for len(parts) > 2 {
100+ parts = parts[1:]
101+ parentDomain := strings.Join(parts, ".")
102+ addDomainRules(parentDomain)
103+ }
104+
90105 // Convert rules to JSON
91106 rulesJSON, err := json.Marshal(rules)
92107 if err != nil {
@@ -67,7 +67,7 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
67 return ""67 return ""
68 }68 }
69 69
70- // Build rules for this specific domain70+ // Build rules for this specific domain (including parent domains)
71 var rules []CosmeticRule71 var rules []CosmeticRule
72 72
73 // Add generic rules73 // Add generic rules
@@ -77,16 +77,31 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
77 })77 })
78 }78 }
79 79
80- // Add domain-specific rules80+ // Add domain-specific rules for the domain and its parent domains
81- if domainSelectors, exists := ci.domainRules[domain]; exists {81+ addDomainRules := func(domainKey string) {
82- for _, selector := range domainSelectors {82+ if domainKey == "" {
83- rules = append(rules, CosmeticRule{83+ return
84- Domain: domain,84+ }
85- Selector: selector,85+ if domainSelectors, exists := ci.domainRules[domainKey]; exists {
86- })86+ for _, selector := range domainSelectors {
87+ rules = append(rules, CosmeticRule{
88+ Domain: domainKey,
89+ Selector: selector,
90+ })
91+ }
87 }92 }
88 }93 }
89 94
95+ addDomainRules(domain)
96+
97+ // Walk up parent domains to include base-domain rules for subdomains
98+ parts := strings.Split(domain, ".")
99+ for len(parts) > 2 {
100+ parts = parts[1:]
101+ parentDomain := strings.Join(parts, ".")
102+ addDomainRules(parentDomain)
103+ }
104+
90 // Convert rules to JSON105 // Convert rules to JSON
91 rulesJSON, err := json.Marshal(rules)106 rulesJSON, err := json.Marshal(rules)
92 if err != nil {107 if err != nil {
M internal/filtering/cosmetic/injector.js +47 -16
@@ -10,6 +10,17 @@ const CosmeticFilter = (() => {
1010 procedural: [] // Complex selectors with :has(), etc.
1111 };
1212
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+
1324 // Safe anti-breakage scriptlets that avoid WebKit internal interference
1425 const antiBreakage = {
1526 // Safely stub ad services without modifying global setTimeout
@@ -186,16 +197,31 @@ const CosmeticFilter = (() => {
186197 antiBreakage.neutralizeAdPromises();
187198 antiBreakage.cleanupLoadingIndicators();
188199
200+ const hostname = window.location.hostname;
201+
189202 // Parse and categorize rules (with null safety)
190203 if (rules && Array.isArray(rules)) {
191204 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);
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;
198217 }
218+
219+ if (matchesDomain) {
220+ selectors.specific.push(selector);
221+ return;
222+ }
223+
224+ selectors.generic.push(selector);
199225 });
200226 }
201227
@@ -214,15 +240,20 @@ const CosmeticFilter = (() => {
214240
215241 // Performance optimization: batch rule updates
216242 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);
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);
226257 }
227258 };
228259
@@ -242,4 +273,4 @@ window.__dumber_cosmetic_init = (rules) => CosmeticFilter.init(rules);
242273 window.__dumber_cosmetic_update = (rules) => CosmeticFilter.updateRules(rules);
243274 window.__dumber_cosmetic_cleanup = () => CosmeticFilter.cleanup();
244275
245-} // End guard against duplicate injection⏎̸
276+} // End guard against duplicate injection
@@ -10,6 +10,17 @@ const CosmeticFilter = (() => {
10 procedural: [] // Complex selectors with :has(), etc.10 procedural: [] // Complex selectors with :has(), etc.
11 };11 };
12 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+
13 // Safe anti-breakage scriptlets that avoid WebKit internal interference24 // Safe anti-breakage scriptlets that avoid WebKit internal interference
14 const antiBreakage = {25 const antiBreakage = {
15 // Safely stub ad services without modifying global setTimeout26 // Safely stub ad services without modifying global setTimeout
@@ -186,16 +197,31 @@ const CosmeticFilter = (() => {
186 antiBreakage.neutralizeAdPromises();197 antiBreakage.neutralizeAdPromises();
187 antiBreakage.cleanupLoadingIndicators();198 antiBreakage.cleanupLoadingIndicators();
188 199
200+ const hostname = window.location.hostname;
201+
189 // Parse and categorize rules (with null safety)202 // Parse and categorize rules (with null safety)
190 if (rules && Array.isArray(rules)) {203 if (rules && Array.isArray(rules)) {
191 rules.forEach(rule => {204 rules.forEach(rule => {
192- if (rule.includes(':has(') || rule.includes(':not(')) {205+ const selector = normalizeSelector(rule);
193- selectors.procedural.push(rule);206+ if (!selector) {
194- } else if (rule.domain === window.location.hostname) {207+ return;
195- selectors.specific.push(rule.selector);208+ }
196- } else {209+
197- selectors.generic.push(rule.selector);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;
198 }217 }
218+
219+ if (matchesDomain) {
220+ selectors.specific.push(selector);
221+ return;
222+ }
223+
224+ selectors.generic.push(selector);
199 });225 });
200 }226 }
201 227
@@ -214,15 +240,20 @@ const CosmeticFilter = (() => {
214 240
215 // Performance optimization: batch rule updates241 // Performance optimization: batch rule updates
216 const updateRules = (newRules) => {242 const updateRules = (newRules) => {
217- // Diff and apply only new rules243+ const additions = [];
218- const newSelectors = newRules.filter(r =>244+ (newRules || []).forEach(rule => {
219- !selectors.generic.includes(r) &&245+ const selector = normalizeSelector(rule);
220- !selectors.specific.includes(r)246+ if (!selector) {
221- );247+ return;
222-248+ }
223- if (newSelectors.length > 0) {249+ if (!selectors.generic.includes(selector) && !selectors.specific.includes(selector)) {
224- hideElements(newSelectors);250+ additions.push(selector);
225- selectors.generic.push(...newSelectors);251+ }
252+ });
253+
254+ if (additions.length > 0) {
255+ hideElements(additions);
256+ selectors.generic.push(...additions);
226 }257 }
227 };258 };
228 259
@@ -242,4 +273,4 @@ window.__dumber_cosmetic_init = (rules) => CosmeticFilter.init(rules);
242 window.__dumber_cosmetic_update = (rules) => CosmeticFilter.updateRules(rules);273 window.__dumber_cosmetic_update = (rules) => CosmeticFilter.updateRules(rules);
243 window.__dumber_cosmetic_cleanup = () => CosmeticFilter.cleanup();274 window.__dumber_cosmetic_cleanup = () => CosmeticFilter.cleanup();
244 275
245-} // End guard against duplicate injection⏎̸276+} // End guard against duplicate injection
M internal/filtering/manager.go +13 -2
@@ -380,8 +380,19 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) {
380380
381381 fm.compiled = filters
382382
383- // Inject cosmetic rules
384- if err := fm.cosmeticInjector.InjectRules(filters.CosmeticRules); err != nil {
383+ // Inject cosmetic rules (domain-specific + generic hiding under empty key)
384+ cosmeticRulesWithGeneric := make(map[string][]string)
385+ for domain, selectors := range filters.CosmeticRules {
386+ cosmeticRulesWithGeneric[domain] = selectors
387+ }
388+ // Add generic hiding rules under empty string key (applies to all domains)
389+ if len(filters.GenericHiding) > 0 {
390+ cosmeticRulesWithGeneric[""] = filters.GenericHiding
391+ logging.Info(fmt.Sprintf("[filtering] Adding %d generic hiding rules to cosmetic injector", len(filters.GenericHiding)))
392+ }
393+ logging.Info(fmt.Sprintf("[filtering] Injecting cosmetic rules: %d domains + %d generic rules",
394+ len(filters.CosmeticRules), len(filters.GenericHiding)))
395+ if err := fm.cosmeticInjector.InjectRules(cosmeticRulesWithGeneric); err != nil {
385396 logging.Error(fmt.Sprintf("Failed to inject cosmetic rules: %v", err))
386397 }
387398
@@ -380,8 +380,19 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) {
380 380
381 fm.compiled = filters381 fm.compiled = filters
382 382
383- // Inject cosmetic rules383+ // Inject cosmetic rules (domain-specific + generic hiding under empty key)
384- if err := fm.cosmeticInjector.InjectRules(filters.CosmeticRules); err != nil {384+ cosmeticRulesWithGeneric := make(map[string][]string)
385+ for domain, selectors := range filters.CosmeticRules {
386+ cosmeticRulesWithGeneric[domain] = selectors
387+ }
388+ // Add generic hiding rules under empty string key (applies to all domains)
389+ if len(filters.GenericHiding) > 0 {
390+ cosmeticRulesWithGeneric[""] = filters.GenericHiding
391+ logging.Info(fmt.Sprintf("[filtering] Adding %d generic hiding rules to cosmetic injector", len(filters.GenericHiding)))
392+ }
393+ logging.Info(fmt.Sprintf("[filtering] Injecting cosmetic rules: %d domains + %d generic rules",
394+ len(filters.CosmeticRules), len(filters.GenericHiding)))
395+ if err := fm.cosmeticInjector.InjectRules(cosmeticRulesWithGeneric); err != nil {
385 logging.Error(fmt.Sprintf("Failed to inject cosmetic rules: %v", err))396 logging.Error(fmt.Sprintf("Failed to inject cosmetic rules: %v", err))
386 }397 }
387 398

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close