v0.15.0 9006957

injector.go RawHistoryFolder

1package cosmetic
2
3import (
4	_ "embed"
5	"encoding/json"
6	"fmt"
7	"strings"
8
9	"github.com/bnema/dumber/internal/logging"
10)
11
12//go:embed injector.js
13var cosmeticScript string
14
15// CosmeticInjector manages cosmetic filter injection
16type CosmeticInjector struct {
17	domainRules   map[string][]string
18	genericRules  []string
19	scriptEnabled bool
20}
21
22// CosmeticRule represents a cosmetic filter rule for JavaScript injection
23type CosmeticRule struct {
24	Domain   string `json:"domain,omitempty"`
25	Selector string `json:"selector"`
26}
27
28// NewCosmeticInjector creates a new cosmetic filter injector
29func NewCosmeticInjector() *CosmeticInjector {
30	return &CosmeticInjector{
31		domainRules:   make(map[string][]string),
32		genericRules:  make([]string, 0),
33		scriptEnabled: true,
34	}
35}
36
37// InjectRules stores cosmetic rules for domain-specific filtering
38func (ci *CosmeticInjector) InjectRules(rules map[string][]string) error {
39	if !ci.scriptEnabled {
40		return fmt.Errorf("cosmetic filtering disabled")
41	}
42
43	// Clear existing rules
44	ci.domainRules = make(map[string][]string)
45	ci.genericRules = make([]string, 0)
46
47	// Process rules
48	for domain, selectors := range rules {
49		if domain == "" {
50			// Generic rules (apply to all domains)
51			ci.genericRules = append(ci.genericRules, selectors...)
52		} else {
53			// Domain-specific rules
54			ci.domainRules[domain] = selectors
55		}
56	}
57
58	logging.Debug(fmt.Sprintf("Injected %d domain-specific rules and %d generic rules",
59		len(ci.domainRules), len(ci.genericRules)))
60
61	return nil
62}
63
64// GetScriptForDomain returns the complete cosmetic filtering script for a domain
65func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
66	if !ci.scriptEnabled {
67		return ""
68	}
69
70	// Build rules for this specific domain (including parent domains)
71	var rules []CosmeticRule
72
73	// Add generic rules
74	for _, selector := range ci.genericRules {
75		rules = append(rules, CosmeticRule{
76			Selector: selector,
77		})
78	}
79
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			}
92		}
93	}
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
105	// Convert rules to JSON
106	rulesJSON, err := json.Marshal(rules)
107	if err != nil {
108		logging.Error(fmt.Sprintf("Failed to marshal cosmetic rules: %v", err))
109		return cosmeticScript
110	}
111
112	// Build the complete script with domain-specific rules
113	initScript := fmt.Sprintf(`
114// Initialize cosmetic filtering for domain: %s
115(function() {
116    if (typeof window.__dumber_cosmetic_init === 'function') {
117        window.__dumber_cosmetic_init(%s);
118        if (window.self === window.top) {
119            console.log('[dumber] Cosmetic filtering initialized with %d rules');
120        }
121    } else {
122        if (window.self === window.top) {
123            console.warn('[dumber] Cosmetic filtering not available');
124        }
125    }
126})();`, domain, string(rulesJSON), len(rules))
127
128	return cosmeticScript + "\n" + initScript
129}
130
131// UpdateRulesForDomain adds new rules for a specific domain
132func (ci *CosmeticInjector) UpdateRulesForDomain(domain string, newSelectors []string) {
133	if !ci.scriptEnabled {
134		return
135	}
136
137	if len(newSelectors) == 0 {
138		return
139	}
140
141	if domain == "" {
142		// Add to generic rules
143		ci.genericRules = append(ci.genericRules, newSelectors...)
144	} else {
145		// Add to domain-specific rules
146		if _, exists := ci.domainRules[domain]; !exists {
147			ci.domainRules[domain] = make([]string, 0)
148		}
149		ci.domainRules[domain] = append(ci.domainRules[domain], newSelectors...)
150	}
151
152	logging.Debug(fmt.Sprintf("Added %d new cosmetic rules for domain: %s", len(newSelectors), domain))
153}
154
155// GetUpdateScript returns JavaScript to update rules dynamically
156func (ci *CosmeticInjector) GetUpdateScript(newSelectors []string) string {
157	if !ci.scriptEnabled || len(newSelectors) == 0 {
158		return ""
159	}
160
161	selectorsJSON, err := json.Marshal(newSelectors)
162	if err != nil {
163		logging.Error(fmt.Sprintf("Failed to marshal update selectors: %v", err))
164		return ""
165	}
166
167	return fmt.Sprintf(`
168(function() {
169    if (typeof window.__dumber_cosmetic_update === 'function') {
170        window.__dumber_cosmetic_update(%s);
171        if (window.self === window.top) {
172            console.log('[dumber] Updated cosmetic rules with %d new selectors');
173        }
174    }
175})();`, string(selectorsJSON), len(newSelectors))
176}
177
178// GetCleanupScript returns JavaScript to cleanup cosmetic filtering on navigation
179func (ci *CosmeticInjector) GetCleanupScript() string {
180	return `
181(function() {
182    if (typeof window.__dumber_cosmetic_cleanup === 'function') {
183        window.__dumber_cosmetic_cleanup();
184        if (window.self === window.top) {
185            console.log('[dumber] Cosmetic filtering cleaned up');
186        }
187    }
188})();`
189}
190
191// GetRulesForDomain returns all cosmetic rules that apply to a domain
192func (ci *CosmeticInjector) GetRulesForDomain(domain string) []string {
193	var rules []string
194
195	// Add generic rules
196	rules = append(rules, ci.genericRules...)
197
198	// Add domain-specific rules
199	if domainSelectors, exists := ci.domainRules[domain]; exists {
200		rules = append(rules, domainSelectors...)
201	}
202
203	// Also check for subdomain matches
204	domainParts := strings.Split(domain, ".")
205	if len(domainParts) > 2 {
206		// Try parent domain (e.g., "example.com" for "www.example.com")
207		parentDomain := strings.Join(domainParts[1:], ".")
208		if parentSelectors, exists := ci.domainRules[parentDomain]; exists {
209			rules = append(rules, parentSelectors...)
210		}
211	}
212
213	return rules
214}
215
216// Enable enables or disables cosmetic filtering
217func (ci *CosmeticInjector) Enable(enabled bool) {
218	ci.scriptEnabled = enabled
219	if enabled {
220		logging.Info("Cosmetic filtering enabled")
221	} else {
222		logging.Info("Cosmetic filtering disabled")
223	}
224}
225
226// IsEnabled returns whether cosmetic filtering is enabled
227func (ci *CosmeticInjector) IsEnabled() bool {
228	return ci.scriptEnabled
229}
230
231// GetStats returns statistics about loaded cosmetic rules
232func (ci *CosmeticInjector) GetStats() map[string]interface{} {
233	domainCount := len(ci.domainRules)
234	totalRules := len(ci.genericRules)
235
236	for _, selectors := range ci.domainRules {
237		totalRules += len(selectors)
238	}
239
240	return map[string]interface{}{
241		"enabled":       ci.scriptEnabled,
242		"generic_rules": len(ci.genericRules),
243		"domain_count":  domainCount,
244		"total_rules":   totalRules,
245	}
246}

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