diff --git a/css_filter_test.go b/css_filter_test.go index 1cfe066..7d7ad5d 100644 --- a/css_filter_test.go +++ b/css_filter_test.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "strings" "testing" ) @@ -166,3 +168,44 @@ func TestFilterSelectorsAgainstHTML(t *testing.T) { } } } + +func TestBuildElementHidingCSSChunking(t *testing.T) { + // Build a selector list larger than maxSelectorsPerRule + n := maxSelectorsPerRule + 500 + selectors := make([]string, n) + for i := range selectors { + selectors[i] = fmt.Sprintf(".sel-%d", i) + } + + css := buildElementHidingCSS(selectors) + + // Should produce 2 rules + count := strings.Count(css, "display: none !important") + if count != 2 { + t.Errorf("expected 2 CSS rules, got %d", count) + } + + // First selector and last selector should both be present + if !strings.Contains(css, ".sel-0") { + t.Error("first selector missing") + } + if !strings.Contains(css, fmt.Sprintf(".sel-%d", n-1)) { + t.Error("last selector missing") + } +} + +func TestBuildElementHidingCSSSmallList(t *testing.T) { + css := buildElementHidingCSS([]string{".a", ".b", ".c"}) + + count := strings.Count(css, "display: none !important") + if count != 1 { + t.Errorf("expected 1 CSS rule for small list, got %d", count) + } +} + +func TestBuildElementHidingCSSEmpty(t *testing.T) { + css := buildElementHidingCSS(nil) + if css != "" { + t.Errorf("expected empty string for nil selectors, got %q", css) + } +} diff --git a/elemhide_inject.go b/elemhide_inject.go index 5252959..6b1bb27 100644 --- a/elemhide_inject.go +++ b/elemhide_inject.go @@ -223,13 +223,31 @@ func mergeElementHidingSelectors(baseline, user *blocklist.ElementHiding, userRS return selectors } +// maxSelectorsPerRule limits the number of selectors in a single CSS rule. +// Chrome truncates rules that exceed its internal selector limit (~4096), +// silently breaking element hiding. Chunking into multiple rules avoids this. +const maxSelectorsPerRule = 4096 + // buildElementHidingCSS produces a display:none stylesheet from a list of -// CSS selectors. Returns empty string if the list is empty. +// CSS selectors. Rules are chunked to stay within browser selector limits. +// Returns empty string if the list is empty. func buildElementHidingCSS(selectors []string) string { if len(selectors) == 0 { return "" } - return strings.Join(selectors, ",\n") + " {\n display: none !important;\n}\n" + if len(selectors) <= maxSelectorsPerRule { + return strings.Join(selectors, ",\n") + " {\n display: none !important;\n}\n" + } + var b strings.Builder + for i := 0; i < len(selectors); i += maxSelectorsPerRule { + end := i + maxSelectorsPerRule + if end > len(selectors) { + end = len(selectors) + } + b.WriteString(strings.Join(selectors[i:end], ",\n")) + b.WriteString(" {\n display: none !important;\n}\n") + } + return b.String() } // injectBeforeClose inserts content before the first found closing tag,