v0.7.0 31c7fae

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
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
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			})
87		}
88	}
89
90	// Convert rules to JSON
91	rulesJSON, err := json.Marshal(rules)
92	if err != nil {
93		logging.Error(fmt.Sprintf("Failed to marshal cosmetic rules: %v", err))
94		return cosmeticScript
95	}
96
97	// Build the complete script with domain-specific rules
98	initScript := fmt.Sprintf(`
99// Initialize cosmetic filtering for domain: %s
100(function() {
101    if (typeof window.__dumber_cosmetic_init === 'function') {
102        window.__dumber_cosmetic_init(%s);
103        console.log('[dumber] Cosmetic filtering initialized with %d rules');
104    } else {
105        console.warn('[dumber] Cosmetic filtering not available');
106    }
107})();`, domain, string(rulesJSON), len(rules))
108
109	return cosmeticScript + "\n" + initScript
110}
111
112// UpdateRulesForDomain adds new rules for a specific domain
113func (ci *CosmeticInjector) UpdateRulesForDomain(domain string, newSelectors []string) {
114	if !ci.scriptEnabled {
115		return
116	}
117
118	if len(newSelectors) == 0 {
119		return
120	}
121
122	if domain == "" {
123		// Add to generic rules
124		ci.genericRules = append(ci.genericRules, newSelectors...)
125	} else {
126		// Add to domain-specific rules
127		if _, exists := ci.domainRules[domain]; !exists {
128			ci.domainRules[domain] = make([]string, 0)
129		}
130		ci.domainRules[domain] = append(ci.domainRules[domain], newSelectors...)
131	}
132
133	logging.Debug(fmt.Sprintf("Added %d new cosmetic rules for domain: %s", len(newSelectors), domain))
134}
135
136// GetUpdateScript returns JavaScript to update rules dynamically
137func (ci *CosmeticInjector) GetUpdateScript(newSelectors []string) string {
138	if !ci.scriptEnabled || len(newSelectors) == 0 {
139		return ""
140	}
141
142	selectorsJSON, err := json.Marshal(newSelectors)
143	if err != nil {
144		logging.Error(fmt.Sprintf("Failed to marshal update selectors: %v", err))
145		return ""
146	}
147
148	return fmt.Sprintf(`
149(function() {
150    if (typeof window.__dumber_cosmetic_update === 'function') {
151        window.__dumber_cosmetic_update(%s);
152        console.log('[dumber] Updated cosmetic rules with %d new selectors');
153    }
154})();`, string(selectorsJSON), len(newSelectors))
155}
156
157// GetCleanupScript returns JavaScript to cleanup cosmetic filtering on navigation
158func (ci *CosmeticInjector) GetCleanupScript() string {
159	return `
160(function() {
161    if (typeof window.__dumber_cosmetic_cleanup === 'function') {
162        window.__dumber_cosmetic_cleanup();
163        console.log('[dumber] Cosmetic filtering cleaned up');
164    }
165})();`
166}
167
168// GetRulesForDomain returns all cosmetic rules that apply to a domain
169func (ci *CosmeticInjector) GetRulesForDomain(domain string) []string {
170	var rules []string
171
172	// Add generic rules
173	rules = append(rules, ci.genericRules...)
174
175	// Add domain-specific rules
176	if domainSelectors, exists := ci.domainRules[domain]; exists {
177		rules = append(rules, domainSelectors...)
178	}
179
180	// Also check for subdomain matches
181	domainParts := strings.Split(domain, ".")
182	if len(domainParts) > 2 {
183		// Try parent domain (e.g., "example.com" for "www.example.com")
184		parentDomain := strings.Join(domainParts[1:], ".")
185		if parentSelectors, exists := ci.domainRules[parentDomain]; exists {
186			rules = append(rules, parentSelectors...)
187		}
188	}
189
190	return rules
191}
192
193// Enable enables or disables cosmetic filtering
194func (ci *CosmeticInjector) Enable(enabled bool) {
195	ci.scriptEnabled = enabled
196	if enabled {
197		logging.Info("Cosmetic filtering enabled")
198	} else {
199		logging.Info("Cosmetic filtering disabled")
200	}
201}
202
203// IsEnabled returns whether cosmetic filtering is enabled
204func (ci *CosmeticInjector) IsEnabled() bool {
205	return ci.scriptEnabled
206}
207
208// GetStats returns statistics about loaded cosmetic rules
209func (ci *CosmeticInjector) GetStats() map[string]interface{} {
210	domainCount := len(ci.domainRules)
211	totalRules := len(ci.genericRules)
212
213	for _, selectors := range ci.domainRules {
214		totalRules += len(selectors)
215	}
216
217	return map[string]interface{}{
218		"enabled":       ci.scriptEnabled,
219		"generic_rules": len(ci.genericRules),
220		"domain_count":  domainCount,
221		"total_rules":   totalRules,
222	}
223}

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