refactor(systemviews): tighten hexagonal boundaries
- commit
- 1ed380e2522beff943b09dd2b8a0034e99663b40
- author
- brice · · 2026-04-24 07:33 UTC
- parent
- 9d4fa93
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +190 -104
M internal/application/usecase/save_webui_config.go +13 -7
| @@ -42,9 +42,7 @@ func normalizeWebUIConfig(cfg port.WebUIConfig) port.WebUIConfig { | |||
| 42 | 42 | trimmedKey := strings.TrimSpace(key) | |
| 43 | 43 | shortcut.URL = strings.TrimSpace(shortcut.URL) | |
| 44 | 44 | shortcut.Description = strings.TrimSpace(shortcut.Description) | |
| 45 | - | if trimmedKey != "" { | |
| 46 | - | normalized[trimmedKey] = shortcut | |
| 47 | - | } | |
| 45 | + | normalized[trimmedKey] = shortcut | |
| 48 | 46 | } | |
| 49 | 47 | cfg.SearchShortcuts = normalized | |
| 50 | 48 | } | |
| @@ -65,10 +63,8 @@ func validateWebUIConfig(cfg port.WebUIConfig) error { | |||
| 65 | 63 | errs = append(errs, validation.ValidateFontFamily("appearance.serif_font", cfg.Appearance.SerifFont)...) | |
| 66 | 64 | errs = append(errs, validation.ValidateFontFamily("appearance.monospace_font", cfg.Appearance.MonospaceFont)...) | |
| 67 | 65 | ||
| 68 | - | if cfg.DefaultSearchEngine == "" { | |
| 69 | - | errs = append(errs, "default_search_engine cannot be empty") | |
| 70 | - | } else if !strings.Contains(cfg.DefaultSearchEngine, "%s") { | |
| 71 | - | errs = append(errs, "default_search_engine must contain %s placeholder for the search query") | |
| 66 | + | for _, err := range validation.ValidateShortcutURL(cfg.DefaultSearchEngine) { | |
| 67 | + | errs = append(errs, fmt.Sprintf("default_search_engine: %s", err)) | |
| 72 | 68 | } | |
| 73 | 69 | ||
| 74 | 70 | for key, shortcut := range cfg.SearchShortcuts { | |
| @@ -83,6 +79,16 @@ func validateWebUIConfig(cfg port.WebUIConfig) error { | |||
| 83 | 79 | } | |
| 84 | 80 | } | |
| 85 | 81 | ||
| 82 | + | switch cfg.Performance.Profile { | |
| 83 | + | case "", "default", "lite", "max", "custom": | |
| 84 | + | // ok | |
| 85 | + | default: | |
| 86 | + | errs = append(errs, fmt.Sprintf( | |
| 87 | + | "performance.profile must be one of: default, lite, max, custom (got: %s)", | |
| 88 | + | cfg.Performance.Profile, | |
| 89 | + | )) | |
| 90 | + | } | |
| 91 | + | ||
| 86 | 92 | switch cfg.Appearance.ColorScheme { | |
| 87 | 93 | case "default", "prefer-dark", "prefer-light": | |
| 88 | 94 | // ok | |
| @@ -42,9 +42,7 @@ func normalizeWebUIConfig(cfg port.WebUIConfig) port.WebUIConfig { | |||||
| 42 | trimmedKey := strings.TrimSpace(key) | 42 | trimmedKey := strings.TrimSpace(key) | ||
| 43 | shortcut.URL = strings.TrimSpace(shortcut.URL) | 43 | shortcut.URL = strings.TrimSpace(shortcut.URL) | ||
| 44 | shortcut.Description = strings.TrimSpace(shortcut.Description) | 44 | shortcut.Description = strings.TrimSpace(shortcut.Description) | ||
| 45 | - | if trimmedKey != "" { | 45 | + | normalized[trimmedKey] = shortcut |
| 46 | - | normalized[trimmedKey] = shortcut | |||
| 47 | - | } | |||
| 48 | } | 46 | } | ||
| 49 | cfg.SearchShortcuts = normalized | 47 | cfg.SearchShortcuts = normalized | ||
| 50 | } | 48 | } | ||
| @@ -65,10 +63,8 @@ func validateWebUIConfig(cfg port.WebUIConfig) error { | |||||
| 65 | errs = append(errs, validation.ValidateFontFamily("appearance.serif_font", cfg.Appearance.SerifFont)...) | 63 | errs = append(errs, validation.ValidateFontFamily("appearance.serif_font", cfg.Appearance.SerifFont)...) | ||
| 66 | errs = append(errs, validation.ValidateFontFamily("appearance.monospace_font", cfg.Appearance.MonospaceFont)...) | 64 | errs = append(errs, validation.ValidateFontFamily("appearance.monospace_font", cfg.Appearance.MonospaceFont)...) | ||
| 67 | 65 | ||||
| 68 | - | if cfg.DefaultSearchEngine == "" { | 66 | + | for _, err := range validation.ValidateShortcutURL(cfg.DefaultSearchEngine) { |
| 69 | - | errs = append(errs, "default_search_engine cannot be empty") | 67 | + | errs = append(errs, fmt.Sprintf("default_search_engine: %s", err)) |
| 70 | - | } else if !strings.Contains(cfg.DefaultSearchEngine, "%s") { | |||
| 71 | - | errs = append(errs, "default_search_engine must contain %s placeholder for the search query") | |||
| 72 | } | 68 | } | ||
| 73 | 69 | ||||
| 74 | for key, shortcut := range cfg.SearchShortcuts { | 70 | for key, shortcut := range cfg.SearchShortcuts { | ||
| @@ -83,6 +79,16 @@ func validateWebUIConfig(cfg port.WebUIConfig) error { | |||||
| 83 | } | 79 | } | ||
| 84 | } | 80 | } | ||
| 85 | 81 | ||||
| 82 | + | switch cfg.Performance.Profile { | |||
| 83 | + | case "", "default", "lite", "max", "custom": | |||
| 84 | + | // ok | |||
| 85 | + | default: | |||
| 86 | + | errs = append(errs, fmt.Sprintf( | |||
| 87 | + | "performance.profile must be one of: default, lite, max, custom (got: %s)", | |||
| 88 | + | cfg.Performance.Profile, | |||
| 89 | + | )) | |||
| 90 | + | } | |||
| 91 | + | ||||
| 86 | switch cfg.Appearance.ColorScheme { | 92 | switch cfg.Appearance.ColorScheme { | ||
| 87 | case "default", "prefer-dark", "prefer-light": | 93 | case "default", "prefer-dark", "prefer-light": | ||
| 88 | // ok | 94 | // ok | ||
A internal/application/usecase/save_webui_config_test.go +111 -0
| @@ -0,0 +1,111 @@ | |||
| 1 | + | package usecase_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | func TestSaveWebUIConfigUseCase_Validation(t *testing.T) { | |
| 13 | + | tests := []struct { | |
| 14 | + | name string | |
| 15 | + | mutate func(*port.WebUIConfig) | |
| 16 | + | wantErr string | |
| 17 | + | }{ | |
| 18 | + | { | |
| 19 | + | name: "rejects invalid default search URL", | |
| 20 | + | mutate: func(cfg *port.WebUIConfig) { | |
| 21 | + | cfg.DefaultSearchEngine = "not-a-url-%s" | |
| 22 | + | }, | |
| 23 | + | wantErr: "default_search_engine", | |
| 24 | + | }, | |
| 25 | + | { | |
| 26 | + | name: "rejects blank search shortcut key", | |
| 27 | + | mutate: func(cfg *port.WebUIConfig) { | |
| 28 | + | cfg.SearchShortcuts[" "] = port.SearchShortcut{URL: "https://example.com?q=%s", Description: "Example"} | |
| 29 | + | }, | |
| 30 | + | wantErr: "shortcut key cannot be empty", | |
| 31 | + | }, | |
| 32 | + | { | |
| 33 | + | name: "rejects invalid performance profile", | |
| 34 | + | mutate: func(cfg *port.WebUIConfig) { | |
| 35 | + | cfg.Performance.Profile = "turbo" | |
| 36 | + | }, | |
| 37 | + | wantErr: "performance.profile", | |
| 38 | + | }, | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | for _, tt := range tests { | |
| 42 | + | t.Run(tt.name, func(t *testing.T) { | |
| 43 | + | cfg := validWebUIConfig() | |
| 44 | + | tt.mutate(&cfg) | |
| 45 | + | uc := usecase.NewSaveWebUIConfigUseCase(fakeWebUIConfigSaver{}) | |
| 46 | + | ||
| 47 | + | err := uc.Execute(context.Background(), cfg) | |
| 48 | + | ||
| 49 | + | require.Error(t, err) | |
| 50 | + | require.Contains(t, err.Error(), tt.wantErr) | |
| 51 | + | }) | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | func TestSaveWebUIConfigUseCase_NormalizesAndSavesValidConfig(t *testing.T) { | |
| 56 | + | saver := &capturingWebUIConfigSaver{} | |
| 57 | + | uc := usecase.NewSaveWebUIConfigUseCase(saver) | |
| 58 | + | cfg := validWebUIConfig() | |
| 59 | + | cfg.SearchShortcuts[" ddg "] = port.SearchShortcut{URL: " https://duckduckgo.com/?q=%s ", Description: " DuckDuckGo "} | |
| 60 | + | ||
| 61 | + | err := uc.Execute(context.Background(), cfg) | |
| 62 | + | ||
| 63 | + | require.NoError(t, err) | |
| 64 | + | require.Contains(t, saver.saved.SearchShortcuts, "ddg") | |
| 65 | + | require.Equal(t, "https://duckduckgo.com/?q=%s", saver.saved.SearchShortcuts["ddg"].URL) | |
| 66 | + | require.Equal(t, "DuckDuckGo", saver.saved.SearchShortcuts["ddg"].Description) | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func validWebUIConfig() port.WebUIConfig { | |
| 70 | + | palette := port.ColorPalette{ | |
| 71 | + | Background: "#ffffff", | |
| 72 | + | Surface: "#f8f8f8", | |
| 73 | + | SurfaceVariant: "#eeeeee", | |
| 74 | + | Text: "#111111", | |
| 75 | + | Muted: "#666666", | |
| 76 | + | Accent: "#0055ff", | |
| 77 | + | Border: "#dddddd", | |
| 78 | + | } | |
| 79 | + | return port.WebUIConfig{ | |
| 80 | + | Appearance: port.WebUIAppearanceConfig{ | |
| 81 | + | SansFont: "Inter", | |
| 82 | + | SerifFont: "Georgia", | |
| 83 | + | MonospaceFont: "JetBrains Mono", | |
| 84 | + | DefaultFontSize: 16, | |
| 85 | + | ColorScheme: "prefer-dark", | |
| 86 | + | LightPalette: palette, | |
| 87 | + | DarkPalette: palette, | |
| 88 | + | }, | |
| 89 | + | Performance: port.WebUIPerformanceConfig{ | |
| 90 | + | Profile: "default", | |
| 91 | + | }, | |
| 92 | + | DefaultUIScale: 1, | |
| 93 | + | DefaultSearchEngine: "https://duckduckgo.com/?q=%s", | |
| 94 | + | SearchShortcuts: map[string]port.SearchShortcut{}, | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | type fakeWebUIConfigSaver struct{} | |
| 99 | + | ||
| 100 | + | func (fakeWebUIConfigSaver) SaveWebUIConfig(context.Context, port.WebUIConfig) error { | |
| 101 | + | return nil | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | type capturingWebUIConfigSaver struct { | |
| 105 | + | saved port.WebUIConfig | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func (s *capturingWebUIConfigSaver) SaveWebUIConfig(_ context.Context, cfg port.WebUIConfig) error { | |
| 109 | + | s.saved = cfg | |
| 110 | + | return nil | |
| 111 | + | } | |
| @@ -0,0 +1,111 @@ | |||||
| 1 | + | package usecase_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | func TestSaveWebUIConfigUseCase_Validation(t *testing.T) { | |||
| 13 | + | tests := []struct { | |||
| 14 | + | name string | |||
| 15 | + | mutate func(*port.WebUIConfig) | |||
| 16 | + | wantErr string | |||
| 17 | + | }{ | |||
| 18 | + | { | |||
| 19 | + | name: "rejects invalid default search URL", | |||
| 20 | + | mutate: func(cfg *port.WebUIConfig) { | |||
| 21 | + | cfg.DefaultSearchEngine = "not-a-url-%s" | |||
| 22 | + | }, | |||
| 23 | + | wantErr: "default_search_engine", | |||
| 24 | + | }, | |||
| 25 | + | { | |||
| 26 | + | name: "rejects blank search shortcut key", | |||
| 27 | + | mutate: func(cfg *port.WebUIConfig) { | |||
| 28 | + | cfg.SearchShortcuts[" "] = port.SearchShortcut{URL: "https://example.com?q=%s", Description: "Example"} | |||
| 29 | + | }, | |||
| 30 | + | wantErr: "shortcut key cannot be empty", | |||
| 31 | + | }, | |||
| 32 | + | { | |||
| 33 | + | name: "rejects invalid performance profile", | |||
| 34 | + | mutate: func(cfg *port.WebUIConfig) { | |||
| 35 | + | cfg.Performance.Profile = "turbo" | |||
| 36 | + | }, | |||
| 37 | + | wantErr: "performance.profile", | |||
| 38 | + | }, | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | for _, tt := range tests { | |||
| 42 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 43 | + | cfg := validWebUIConfig() | |||
| 44 | + | tt.mutate(&cfg) | |||
| 45 | + | uc := usecase.NewSaveWebUIConfigUseCase(fakeWebUIConfigSaver{}) | |||
| 46 | + | ||||
| 47 | + | err := uc.Execute(context.Background(), cfg) | |||
| 48 | + | ||||
| 49 | + | require.Error(t, err) | |||
| 50 | + | require.Contains(t, err.Error(), tt.wantErr) | |||
| 51 | + | }) | |||
| 52 | + | } | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | func TestSaveWebUIConfigUseCase_NormalizesAndSavesValidConfig(t *testing.T) { | |||
| 56 | + | saver := &capturingWebUIConfigSaver{} | |||
| 57 | + | uc := usecase.NewSaveWebUIConfigUseCase(saver) | |||
| 58 | + | cfg := validWebUIConfig() | |||
| 59 | + | cfg.SearchShortcuts[" ddg "] = port.SearchShortcut{URL: " https://duckduckgo.com/?q=%s ", Description: " DuckDuckGo "} | |||
| 60 | + | ||||
| 61 | + | err := uc.Execute(context.Background(), cfg) | |||
| 62 | + | ||||
| 63 | + | require.NoError(t, err) | |||
| 64 | + | require.Contains(t, saver.saved.SearchShortcuts, "ddg") | |||
| 65 | + | require.Equal(t, "https://duckduckgo.com/?q=%s", saver.saved.SearchShortcuts["ddg"].URL) | |||
| 66 | + | require.Equal(t, "DuckDuckGo", saver.saved.SearchShortcuts["ddg"].Description) | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | func validWebUIConfig() port.WebUIConfig { | |||
| 70 | + | palette := port.ColorPalette{ | |||
| 71 | + | Background: "#ffffff", | |||
| 72 | + | Surface: "#f8f8f8", | |||
| 73 | + | SurfaceVariant: "#eeeeee", | |||
| 74 | + | Text: "#111111", | |||
| 75 | + | Muted: "#666666", | |||
| 76 | + | Accent: "#0055ff", | |||
| 77 | + | Border: "#dddddd", | |||
| 78 | + | } | |||
| 79 | + | return port.WebUIConfig{ | |||
| 80 | + | Appearance: port.WebUIAppearanceConfig{ | |||
| 81 | + | SansFont: "Inter", | |||
| 82 | + | SerifFont: "Georgia", | |||
| 83 | + | MonospaceFont: "JetBrains Mono", | |||
| 84 | + | DefaultFontSize: 16, | |||
| 85 | + | ColorScheme: "prefer-dark", | |||
| 86 | + | LightPalette: palette, | |||
| 87 | + | DarkPalette: palette, | |||
| 88 | + | }, | |||
| 89 | + | Performance: port.WebUIPerformanceConfig{ | |||
| 90 | + | Profile: "default", | |||
| 91 | + | }, | |||
| 92 | + | DefaultUIScale: 1, | |||
| 93 | + | DefaultSearchEngine: "https://duckduckgo.com/?q=%s", | |||
| 94 | + | SearchShortcuts: map[string]port.SearchShortcut{}, | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | type fakeWebUIConfigSaver struct{} | |||
| 99 | + | ||||
| 100 | + | func (fakeWebUIConfigSaver) SaveWebUIConfig(context.Context, port.WebUIConfig) error { | |||
| 101 | + | return nil | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | type capturingWebUIConfigSaver struct { | |||
| 105 | + | saved port.WebUIConfig | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | func (s *capturingWebUIConfigSaver) SaveWebUIConfig(_ context.Context, cfg port.WebUIConfig) error { | |||
| 109 | + | s.saved = cfg | |||
| 110 | + | return nil | |||
| 111 | + | } | |||
M internal/domain/url/normalize.go +29 -1
| @@ -178,7 +178,35 @@ func ExtractDomain(rawURL string) string { | |||
| 178 | 178 | if err != nil || parsed.Host == "" { | |
| 179 | 179 | return "" | |
| 180 | 180 | } | |
| 181 | - | return strings.TrimPrefix(parsed.Host, "www.") | |
| 181 | + | return strings.TrimPrefix(strings.ToLower(parsed.Host), "www.") | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | // CanonicalDomain extracts a normalized domain from either a URL string or a | |
| 185 | + | // host-like domain string. It keeps non-default ports so callers that need to | |
| 186 | + | // round-trip repository domain keys do not lose identity. | |
| 187 | + | func CanonicalDomain(raw string) string { | |
| 188 | + | raw = strings.TrimSpace(strings.ToLower(raw)) | |
| 189 | + | if raw == "" { | |
| 190 | + | return "" | |
| 191 | + | } | |
| 192 | + | if domain := ExtractDomain(raw); domain != "" { | |
| 193 | + | return domain | |
| 194 | + | } | |
| 195 | + | for _, sep := range []string{"/", "?", "#"} { | |
| 196 | + | if before, _, ok := strings.Cut(raw, sep); ok { | |
| 197 | + | raw = before | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | return strings.TrimPrefix(raw, "www.") | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // DisplayDomain returns a user-facing domain label with any port removed. | |
| 204 | + | func DisplayDomain(raw string) string { | |
| 205 | + | domain := CanonicalDomain(raw) | |
| 206 | + | if host, _, err := net.SplitHostPort(domain); err == nil { | |
| 207 | + | return host | |
| 208 | + | } | |
| 209 | + | return domain | |
| 182 | 210 | } | |
| 183 | 211 | ||
| 184 | 212 | // SanitizeDomainForFilename converts a domain to a safe filename with .ico extension. | |
| @@ -178,7 +178,35 @@ func ExtractDomain(rawURL string) string { | |||||
| 178 | if err != nil || parsed.Host == "" { | 178 | if err != nil || parsed.Host == "" { | ||
| 179 | return "" | 179 | return "" | ||
| 180 | } | 180 | } | ||
| 181 | - | return strings.TrimPrefix(parsed.Host, "www.") | 181 | + | return strings.TrimPrefix(strings.ToLower(parsed.Host), "www.") |
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | // CanonicalDomain extracts a normalized domain from either a URL string or a | |||
| 185 | + | // host-like domain string. It keeps non-default ports so callers that need to | |||
| 186 | + | // round-trip repository domain keys do not lose identity. | |||
| 187 | + | func CanonicalDomain(raw string) string { | |||
| 188 | + | raw = strings.TrimSpace(strings.ToLower(raw)) | |||
| 189 | + | if raw == "" { | |||
| 190 | + | return "" | |||
| 191 | + | } | |||
| 192 | + | if domain := ExtractDomain(raw); domain != "" { | |||
| 193 | + | return domain | |||
| 194 | + | } | |||
| 195 | + | for _, sep := range []string{"/", "?", "#"} { | |||
| 196 | + | if before, _, ok := strings.Cut(raw, sep); ok { | |||
| 197 | + | raw = before | |||
| 198 | + | } | |||
| 199 | + | } | |||
| 200 | + | return strings.TrimPrefix(raw, "www.") | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // DisplayDomain returns a user-facing domain label with any port removed. | |||
| 204 | + | func DisplayDomain(raw string) string { | |||
| 205 | + | domain := CanonicalDomain(raw) | |||
| 206 | + | if host, _, err := net.SplitHostPort(domain); err == nil { | |||
| 207 | + | return host | |||
| 208 | + | } | |||
| 209 | + | return domain | |||
| 182 | } | 210 | } | ||
| 183 | 211 | ||||
| 184 | // SanitizeDomainForFilename converts a domain to a safe filename with .ico extension. | 212 | // SanitizeDomainForFilename converts a domain to a safe filename with .ico extension. | ||
M internal/domain/url/normalize_test.go +27 -0
| @@ -692,3 +692,30 @@ func TestExtractOrigin_InvalidURI(t *testing.T) { | |||
| 692 | 692 | }) | |
| 693 | 693 | } | |
| 694 | 694 | } | |
| 695 | + | ||
| 696 | + | func TestCanonicalDomain(t *testing.T) { | |
| 697 | + | tests := []struct { | |
| 698 | + | name string | |
| 699 | + | input string | |
| 700 | + | want string | |
| 701 | + | }{ | |
| 702 | + | {name: "url", input: "https://www.example.com/path", want: "example.com"}, | |
| 703 | + | {name: "url with port", input: "https://www.example.com:8443/path", want: "example.com:8443"}, | |
| 704 | + | {name: "domain", input: "www.example.com", want: "example.com"}, | |
| 705 | + | {name: "domain with path", input: "www.example.com/path?q=1", want: "example.com"}, | |
| 706 | + | } | |
| 707 | + | ||
| 708 | + | for _, tt := range tests { | |
| 709 | + | t.Run(tt.name, func(t *testing.T) { | |
| 710 | + | if got := CanonicalDomain(tt.input); got != tt.want { | |
| 711 | + | t.Errorf("CanonicalDomain(%q) = %q, want %q", tt.input, got, tt.want) | |
| 712 | + | } | |
| 713 | + | }) | |
| 714 | + | } | |
| 715 | + | } | |
| 716 | + | ||
| 717 | + | func TestDisplayDomain(t *testing.T) { | |
| 718 | + | if got := DisplayDomain("https://www.example.com:8443/path"); got != "example.com" { | |
| 719 | + | t.Errorf("DisplayDomain() = %q, want %q", got, "example.com") | |
| 720 | + | } | |
| 721 | + | } | |
| @@ -692,3 +692,30 @@ func TestExtractOrigin_InvalidURI(t *testing.T) { | |||||
| 692 | }) | 692 | }) | ||
| 693 | } | 693 | } | ||
| 694 | } | 694 | } | ||
| 695 | + | ||||
| 696 | + | func TestCanonicalDomain(t *testing.T) { | |||
| 697 | + | tests := []struct { | |||
| 698 | + | name string | |||
| 699 | + | input string | |||
| 700 | + | want string | |||
| 701 | + | }{ | |||
| 702 | + | {name: "url", input: "https://www.example.com/path", want: "example.com"}, | |||
| 703 | + | {name: "url with port", input: "https://www.example.com:8443/path", want: "example.com:8443"}, | |||
| 704 | + | {name: "domain", input: "www.example.com", want: "example.com"}, | |||
| 705 | + | {name: "domain with path", input: "www.example.com/path?q=1", want: "example.com"}, | |||
| 706 | + | } | |||
| 707 | + | ||||
| 708 | + | for _, tt := range tests { | |||
| 709 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 710 | + | if got := CanonicalDomain(tt.input); got != tt.want { | |||
| 711 | + | t.Errorf("CanonicalDomain(%q) = %q, want %q", tt.input, got, tt.want) | |||
| 712 | + | } | |||
| 713 | + | }) | |||
| 714 | + | } | |||
| 715 | + | } | |||
| 716 | + | ||||
| 717 | + | func TestDisplayDomain(t *testing.T) { | |||
| 718 | + | if got := DisplayDomain("https://www.example.com:8443/path"); got != "example.com" { | |||
| 719 | + | t.Errorf("DisplayDomain() = %q, want %q", got, "example.com") | |||
| 720 | + | } | |||
| 721 | + | } | |||
M internal/ui/systemviews/config_actions.go +6 -55
| @@ -7,7 +7,6 @@ import ( | |||
| 7 | 7 | "strings" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/dumber/internal/application/port" | |
| 10 | - | "github.com/bnema/dumber/internal/domain/validation" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| 13 | 12 | const ( | |
| @@ -112,9 +111,6 @@ func (a *App) saveSearchConfig(ctx context.Context, data map[string]string) erro | |||
| 112 | 111 | return err | |
| 113 | 112 | } | |
| 114 | 113 | cfg.DefaultSearchEngine = strings.TrimSpace(data["default_search_engine"]) | |
| 115 | - | if err := validateSearchURL(cfg.DefaultSearchEngine, "default search engine"); err != nil { | |
| 116 | - | return err | |
| 117 | - | } | |
| 118 | 114 | if err := a.saveEditableConfig(ctx, cfg); err != nil { | |
| 119 | 115 | return err | |
| 120 | 116 | } | |
| @@ -127,10 +123,7 @@ func (a *App) createSearchShortcut(ctx context.Context, data map[string]string) | |||
| 127 | 123 | if err != nil { | |
| 128 | 124 | return err | |
| 129 | 125 | } | |
| 130 | - | key, shortcut, err := searchShortcutFromForm(data, "key") | |
| 131 | - | if err != nil { | |
| 132 | - | return err | |
| 133 | - | } | |
| 126 | + | key, shortcut := searchShortcutFromForm(data, "key") | |
| 134 | 127 | cfg.SearchShortcuts = cloneSearchShortcuts(cfg.SearchShortcuts) | |
| 135 | 128 | if _, exists := cfg.SearchShortcuts[key]; exists { | |
| 136 | 129 | return fmt.Errorf("search shortcut %q already exists", key) | |
| @@ -152,10 +145,7 @@ func (a *App) updateSearchShortcut(ctx context.Context, data map[string]string) | |||
| 152 | 145 | if oldKey == "" { | |
| 153 | 146 | return fmt.Errorf("search shortcut key is required") | |
| 154 | 147 | } | |
| 155 | - | newKey, shortcut, err := searchShortcutFromForm(data, "new_key") | |
| 156 | - | if err != nil { | |
| 157 | - | return err | |
| 158 | - | } | |
| 148 | + | newKey, shortcut := searchShortcutFromForm(data, "new_key") | |
| 159 | 149 | cfg.SearchShortcuts = cloneSearchShortcuts(cfg.SearchShortcuts) | |
| 160 | 150 | if _, exists := cfg.SearchShortcuts[oldKey]; !exists { | |
| 161 | 151 | return fmt.Errorf("search shortcut %q not found", oldKey) | |
| @@ -201,9 +191,6 @@ func (a *App) savePerformanceConfig(ctx context.Context, data map[string]string) | |||
| 201 | 191 | return err | |
| 202 | 192 | } | |
| 203 | 193 | profile := strings.TrimSpace(data["profile"]) | |
| 204 | - | if !validPerformanceProfile(profile) { | |
| 205 | - | return fmt.Errorf("invalid performance profile") | |
| 206 | - | } | |
| 207 | 194 | skiaCPU, err := parseConfigInt(data["skia_cpu_threads"], "Skia CPU threads") | |
| 208 | 195 | if err != nil { | |
| 209 | 196 | return err | |
| @@ -363,46 +350,10 @@ func paletteFromForm(data map[string]string, prefix string) port.ColorPalette { | |||
| 363 | 350 | } | |
| 364 | 351 | } | |
| 365 | 352 | ||
| 366 | - | func searchShortcutFromForm(data map[string]string, keyField string) (string, port.SearchShortcut, error) { | |
| 367 | - | key := strings.TrimSpace(data[keyField]) | |
| 368 | - | url := strings.TrimSpace(data["url"]) | |
| 369 | - | description := strings.TrimSpace(data["description"]) | |
| 370 | - | if err := validateSearchShortcut(key, url, description); err != nil { | |
| 371 | - | return "", port.SearchShortcut{}, err | |
| 372 | - | } | |
| 373 | - | return key, port.SearchShortcut{URL: url, Description: description}, nil | |
| 374 | - | } | |
| 375 | - | ||
| 376 | - | func validateSearchURL(raw, label string) error { | |
| 377 | - | if errs := validation.ValidateShortcutURL(raw); len(errs) > 0 { | |
| 378 | - | return fmt.Errorf("%s: %s", label, strings.Join(errs, "; ")) | |
| 379 | - | } | |
| 380 | - | return nil | |
| 381 | - | } | |
| 382 | - | ||
| 383 | - | func validateSearchShortcut(key, url, description string) error { | |
| 384 | - | var errs []string | |
| 385 | - | for _, err := range validation.ValidateShortcutKey(key) { | |
| 386 | - | errs = append(errs, "key: "+err) | |
| 387 | - | } | |
| 388 | - | for _, err := range validation.ValidateShortcutURL(url) { | |
| 389 | - | errs = append(errs, "url: "+err) | |
| 390 | - | } | |
| 391 | - | for _, err := range validation.ValidateShortcutDescription(description) { | |
| 392 | - | errs = append(errs, "description: "+err) | |
| 393 | - | } | |
| 394 | - | if len(errs) > 0 { | |
| 395 | - | return fmt.Errorf("invalid search shortcut: %s", strings.Join(errs, "; ")) | |
| 396 | - | } | |
| 397 | - | return nil | |
| 398 | - | } | |
| 399 | - | ||
| 400 | - | func validPerformanceProfile(profile string) bool { | |
| 401 | - | switch profile { | |
| 402 | - | case "default", "lite", "max", "custom", "": | |
| 403 | - | return true | |
| 404 | - | default: | |
| 405 | - | return false | |
| 353 | + | func searchShortcutFromForm(data map[string]string, keyField string) (string, port.SearchShortcut) { | |
| 354 | + | return strings.TrimSpace(data[keyField]), port.SearchShortcut{ | |
| 355 | + | URL: strings.TrimSpace(data["url"]), | |
| 356 | + | Description: strings.TrimSpace(data["description"]), | |
| 406 | 357 | } | |
| 407 | 358 | } | |
| 408 | 359 | ||
| @@ -7,7 +7,6 @@ import ( | |||||
| 7 | "strings" | 7 | "strings" | ||
| 8 | 8 | ||||
| 9 | "github.com/bnema/dumber/internal/application/port" | 9 | "github.com/bnema/dumber/internal/application/port" | ||
| 10 | - | "github.com/bnema/dumber/internal/domain/validation" | |||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
| 13 | const ( | 12 | const ( | ||
| @@ -112,9 +111,6 @@ func (a *App) saveSearchConfig(ctx context.Context, data map[string]string) erro | |||||
| 112 | return err | 111 | return err | ||
| 113 | } | 112 | } | ||
| 114 | cfg.DefaultSearchEngine = strings.TrimSpace(data["default_search_engine"]) | 113 | cfg.DefaultSearchEngine = strings.TrimSpace(data["default_search_engine"]) | ||
| 115 | - | if err := validateSearchURL(cfg.DefaultSearchEngine, "default search engine"); err != nil { | |||
| 116 | - | return err | |||
| 117 | - | } | |||
| 118 | if err := a.saveEditableConfig(ctx, cfg); err != nil { | 114 | if err := a.saveEditableConfig(ctx, cfg); err != nil { | ||
| 119 | return err | 115 | return err | ||
| 120 | } | 116 | } | ||
| @@ -127,10 +123,7 @@ func (a *App) createSearchShortcut(ctx context.Context, data map[string]string) | |||||
| 127 | if err != nil { | 123 | if err != nil { | ||
| 128 | return err | 124 | return err | ||
| 129 | } | 125 | } | ||
| 130 | - | key, shortcut, err := searchShortcutFromForm(data, "key") | 126 | + | key, shortcut := searchShortcutFromForm(data, "key") |
| 131 | - | if err != nil { | |||
| 132 | - | return err | |||
| 133 | - | } | |||
| 134 | cfg.SearchShortcuts = cloneSearchShortcuts(cfg.SearchShortcuts) | 127 | cfg.SearchShortcuts = cloneSearchShortcuts(cfg.SearchShortcuts) | ||
| 135 | if _, exists := cfg.SearchShortcuts[key]; exists { | 128 | if _, exists := cfg.SearchShortcuts[key]; exists { | ||
| 136 | return fmt.Errorf("search shortcut %q already exists", key) | 129 | return fmt.Errorf("search shortcut %q already exists", key) | ||
| @@ -152,10 +145,7 @@ func (a *App) updateSearchShortcut(ctx context.Context, data map[string]string) | |||||
| 152 | if oldKey == "" { | 145 | if oldKey == "" { | ||
| 153 | return fmt.Errorf("search shortcut key is required") | 146 | return fmt.Errorf("search shortcut key is required") | ||
| 154 | } | 147 | } | ||
| 155 | - | newKey, shortcut, err := searchShortcutFromForm(data, "new_key") | 148 | + | newKey, shortcut := searchShortcutFromForm(data, "new_key") |
| 156 | - | if err != nil { | |||
| 157 | - | return err | |||
| 158 | - | } | |||
| 159 | cfg.SearchShortcuts = cloneSearchShortcuts(cfg.SearchShortcuts) | 149 | cfg.SearchShortcuts = cloneSearchShortcuts(cfg.SearchShortcuts) | ||
| 160 | if _, exists := cfg.SearchShortcuts[oldKey]; !exists { | 150 | if _, exists := cfg.SearchShortcuts[oldKey]; !exists { | ||
| 161 | return fmt.Errorf("search shortcut %q not found", oldKey) | 151 | return fmt.Errorf("search shortcut %q not found", oldKey) | ||
| @@ -201,9 +191,6 @@ func (a *App) savePerformanceConfig(ctx context.Context, data map[string]string) | |||||
| 201 | return err | 191 | return err | ||
| 202 | } | 192 | } | ||
| 203 | profile := strings.TrimSpace(data["profile"]) | 193 | profile := strings.TrimSpace(data["profile"]) | ||
| 204 | - | if !validPerformanceProfile(profile) { | |||
| 205 | - | return fmt.Errorf("invalid performance profile") | |||
| 206 | - | } | |||
| 207 | skiaCPU, err := parseConfigInt(data["skia_cpu_threads"], "Skia CPU threads") | 194 | skiaCPU, err := parseConfigInt(data["skia_cpu_threads"], "Skia CPU threads") | ||
| 208 | if err != nil { | 195 | if err != nil { | ||
| 209 | return err | 196 | return err | ||
| @@ -363,46 +350,10 @@ func paletteFromForm(data map[string]string, prefix string) port.ColorPalette { | |||||
| 363 | } | 350 | } | ||
| 364 | } | 351 | } | ||
| 365 | 352 | ||||
| 366 | - | func searchShortcutFromForm(data map[string]string, keyField string) (string, port.SearchShortcut, error) { | 353 | + | func searchShortcutFromForm(data map[string]string, keyField string) (string, port.SearchShortcut) { |
| 367 | - | key := strings.TrimSpace(data[keyField]) | 354 | + | return strings.TrimSpace(data[keyField]), port.SearchShortcut{ |
| 368 | - | url := strings.TrimSpace(data["url"]) | 355 | + | URL: strings.TrimSpace(data["url"]), |
| 369 | - | description := strings.TrimSpace(data["description"]) | 356 | + | Description: strings.TrimSpace(data["description"]), |
| 370 | - | if err := validateSearchShortcut(key, url, description); err != nil { | |||
| 371 | - | return "", port.SearchShortcut{}, err | |||
| 372 | - | } | |||
| 373 | - | return key, port.SearchShortcut{URL: url, Description: description}, nil | |||
| 374 | - | } | |||
| 375 | - | ||||
| 376 | - | func validateSearchURL(raw, label string) error { | |||
| 377 | - | if errs := validation.ValidateShortcutURL(raw); len(errs) > 0 { | |||
| 378 | - | return fmt.Errorf("%s: %s", label, strings.Join(errs, "; ")) | |||
| 379 | - | } | |||
| 380 | - | return nil | |||
| 381 | - | } | |||
| 382 | - | ||||
| 383 | - | func validateSearchShortcut(key, url, description string) error { | |||
| 384 | - | var errs []string | |||
| 385 | - | for _, err := range validation.ValidateShortcutKey(key) { | |||
| 386 | - | errs = append(errs, "key: "+err) | |||
| 387 | - | } | |||
| 388 | - | for _, err := range validation.ValidateShortcutURL(url) { | |||
| 389 | - | errs = append(errs, "url: "+err) | |||
| 390 | - | } | |||
| 391 | - | for _, err := range validation.ValidateShortcutDescription(description) { | |||
| 392 | - | errs = append(errs, "description: "+err) | |||
| 393 | - | } | |||
| 394 | - | if len(errs) > 0 { | |||
| 395 | - | return fmt.Errorf("invalid search shortcut: %s", strings.Join(errs, "; ")) | |||
| 396 | - | } | |||
| 397 | - | return nil | |||
| 398 | - | } | |||
| 399 | - | ||||
| 400 | - | func validPerformanceProfile(profile string) bool { | |||
| 401 | - | switch profile { | |||
| 402 | - | case "default", "lite", "max", "custom", "": | |||
| 403 | - | return true | |||
| 404 | - | default: | |||
| 405 | - | return false | |||
| 406 | } | 357 | } | ||
| 407 | } | 358 | } | ||
| 408 | 359 | ||||
M internal/ui/systemviews/history_actions.go +0 -11
| @@ -5,9 +5,6 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "strconv" | |
| 7 | 7 | "strings" | |
| 8 | - | "time" | |
| 9 | - | ||
| 10 | - | historydomain "github.com/bnema/dumber/internal/domain/history" | |
| 11 | 8 | ) | |
| 12 | 9 | ||
| 13 | 10 | const ( | |
| @@ -117,9 +114,6 @@ func (a *App) handleHistoryAction(ctx context.Context, event DOMAction) error { | |||
| 117 | 114 | a.historyNotice = "Deleted history entry" | |
| 118 | 115 | case historyActionDeleteRange: | |
| 119 | 116 | rangeID := strings.TrimSpace(data["range"]) | |
| 120 | - | if !validHistoryRange(rangeID) { | |
| 121 | - | return fmt.Errorf("invalid history cleanup range") | |
| 122 | - | } | |
| 123 | 117 | if err := a.deps.History.DeleteRange(ctx, rangeID); err != nil { | |
| 124 | 118 | return err | |
| 125 | 119 | } | |
| @@ -149,11 +143,6 @@ func (a *App) mountRenderedHTML() error { | |||
| 149 | 143 | return a.deps.DOM.Mount(a.renderedHTML) | |
| 150 | 144 | } | |
| 151 | 145 | ||
| 152 | - | func validHistoryRange(rangeID string) bool { | |
| 153 | - | _, _, ok := historydomain.DeleteRangeCutoff(rangeID, time.Now()) | |
| 154 | - | return ok | |
| 155 | - | } | |
| 156 | - | ||
| 157 | 146 | func historyRangeNotice(rangeID string) string { | |
| 158 | 147 | for _, item := range historyCleanupItems() { | |
| 159 | 148 | if item.RangeID == rangeID { | |
| @@ -5,9 +5,6 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "strconv" | 6 | "strconv" | ||
| 7 | "strings" | 7 | "strings" | ||
| 8 | - | "time" | |||
| 9 | - | ||||
| 10 | - | historydomain "github.com/bnema/dumber/internal/domain/history" | |||
| 11 | ) | 8 | ) | ||
| 12 | 9 | ||||
| 13 | const ( | 10 | const ( | ||
| @@ -117,9 +114,6 @@ func (a *App) handleHistoryAction(ctx context.Context, event DOMAction) error { | |||||
| 117 | a.historyNotice = "Deleted history entry" | 114 | a.historyNotice = "Deleted history entry" | ||
| 118 | case historyActionDeleteRange: | 115 | case historyActionDeleteRange: | ||
| 119 | rangeID := strings.TrimSpace(data["range"]) | 116 | rangeID := strings.TrimSpace(data["range"]) | ||
| 120 | - | if !validHistoryRange(rangeID) { | |||
| 121 | - | return fmt.Errorf("invalid history cleanup range") | |||
| 122 | - | } | |||
| 123 | if err := a.deps.History.DeleteRange(ctx, rangeID); err != nil { | 117 | if err := a.deps.History.DeleteRange(ctx, rangeID); err != nil { | ||
| 124 | return err | 118 | return err | ||
| 125 | } | 119 | } | ||
| @@ -149,11 +143,6 @@ func (a *App) mountRenderedHTML() error { | |||||
| 149 | return a.deps.DOM.Mount(a.renderedHTML) | 143 | return a.deps.DOM.Mount(a.renderedHTML) | ||
| 150 | } | 144 | } | ||
| 151 | 145 | ||||
| 152 | - | func validHistoryRange(rangeID string) bool { | |||
| 153 | - | _, _, ok := historydomain.DeleteRangeCutoff(rangeID, time.Now()) | |||
| 154 | - | return ok | |||
| 155 | - | } | |||
| 156 | - | ||||
| 157 | func historyRangeNotice(rangeID string) string { | 146 | func historyRangeNotice(rangeID string) string { | ||
| 158 | for _, item := range historyCleanupItems() { | 147 | for _, item := range historyCleanupItems() { | ||
| 159 | if item.RangeID == rangeID { | 148 | if item.RangeID == rangeID { | ||
M internal/ui/systemviews/history_view.go +4 -30
| @@ -2,12 +2,11 @@ package systemviews | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | - | "net" | |
| 6 | - | "net/url" | |
| 7 | 5 | "strings" | |
| 8 | 6 | "time" | |
| 9 | 7 | ||
| 10 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | + | browserurl "github.com/bnema/dumber/internal/domain/url" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| 13 | 12 | type historyRenderData struct { | |
| @@ -197,7 +196,7 @@ func countUniqueHistoryDays(entries []*entity.HistoryEntry) int64 { | |||
| 197 | 196 | } | |
| 198 | 197 | ||
| 199 | 198 | func filterHistoryEntriesByDomain(entries []*entity.HistoryEntry, domain string) []*entity.HistoryEntry { | |
| 200 | - | domain = canonicalHistoryDomain(domain) | |
| 199 | + | domain = browserurl.CanonicalDomain(domain) | |
| 201 | 200 | if domain == "" { | |
| 202 | 201 | return entries | |
| 203 | 202 | } | |
| @@ -206,7 +205,7 @@ func filterHistoryEntriesByDomain(entries []*entity.HistoryEntry, domain string) | |||
| 206 | 205 | if entry == nil { | |
| 207 | 206 | continue | |
| 208 | 207 | } | |
| 209 | - | if canonicalHistoryDomain(entry.URL) == domain { | |
| 208 | + | if browserurl.CanonicalDomain(entry.URL) == domain { | |
| 210 | 209 | filtered = append(filtered, entry) | |
| 211 | 210 | } | |
| 212 | 211 | } | |
| @@ -214,11 +213,7 @@ func filterHistoryEntriesByDomain(entries []*entity.HistoryEntry, domain string) | |||
| 214 | 213 | } | |
| 215 | 214 | ||
| 216 | 215 | func displayHistoryDomain(raw string) string { | |
| 217 | - | domain := canonicalHistoryDomain(raw) | |
| 218 | - | if host, _, err := net.SplitHostPort(domain); err == nil { | |
| 219 | - | domain = host | |
| 220 | - | } | |
| 221 | - | return domain | |
| 216 | + | return browserurl.DisplayDomain(raw) | |
| 222 | 217 | } | |
| 223 | 218 | ||
| 224 | 219 | func historyDomainActionKey(domain *entity.DomainStat) string { | |
| @@ -235,27 +230,6 @@ func historyDomainDisplayLabel(domain *entity.DomainStat) string { | |||
| 235 | 230 | return displayHistoryDomain(domain.Domain) | |
| 236 | 231 | } | |
| 237 | 232 | ||
| 238 | - | func canonicalHistoryDomain(raw string) string { | |
| 239 | - | raw = strings.TrimSpace(strings.ToLower(raw)) | |
| 240 | - | if raw == "" { | |
| 241 | - | return "" | |
| 242 | - | } | |
| 243 | - | if parsed, err := url.Parse(raw); err == nil && parsed.Host != "" { | |
| 244 | - | return strings.TrimPrefix(parsed.Host, "www.") | |
| 245 | - | } | |
| 246 | - | if schemeIdx := strings.Index(raw, "://"); schemeIdx >= 0 { | |
| 247 | - | if parsed, err := url.Parse(raw[schemeIdx+3:]); err == nil && parsed.Host != "" { | |
| 248 | - | return strings.TrimPrefix(parsed.Host, "www.") | |
| 249 | - | } | |
| 250 | - | } | |
| 251 | - | for _, sep := range []string{"/", "?", "#"} { | |
| 252 | - | if before, _, ok := strings.Cut(raw, sep); ok { | |
| 253 | - | raw = before | |
| 254 | - | } | |
| 255 | - | } | |
| 256 | - | return strings.TrimPrefix(raw, "www.") | |
| 257 | - | } | |
| 258 | - | ||
| 259 | 233 | func formatHistoryTime(ts time.Time) string { | |
| 260 | 234 | if ts.IsZero() { | |
| 261 | 235 | return "" | |
| @@ -2,12 +2,11 @@ package systemviews | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | - | "net" | |||
| 6 | - | "net/url" | |||
| 7 | "strings" | 5 | "strings" | ||
| 8 | "time" | 6 | "time" | ||
| 9 | 7 | ||||
| 10 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 9 | + | browserurl "github.com/bnema/dumber/internal/domain/url" | |||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
| 13 | type historyRenderData struct { | 12 | type historyRenderData struct { | ||
| @@ -197,7 +196,7 @@ func countUniqueHistoryDays(entries []*entity.HistoryEntry) int64 { | |||||
| 197 | } | 196 | } | ||
| 198 | 197 | ||||
| 199 | func filterHistoryEntriesByDomain(entries []*entity.HistoryEntry, domain string) []*entity.HistoryEntry { | 198 | func filterHistoryEntriesByDomain(entries []*entity.HistoryEntry, domain string) []*entity.HistoryEntry { | ||
| 200 | - | domain = canonicalHistoryDomain(domain) | 199 | + | domain = browserurl.CanonicalDomain(domain) |
| 201 | if domain == "" { | 200 | if domain == "" { | ||
| 202 | return entries | 201 | return entries | ||
| 203 | } | 202 | } | ||
| @@ -206,7 +205,7 @@ func filterHistoryEntriesByDomain(entries []*entity.HistoryEntry, domain string) | |||||
| 206 | if entry == nil { | 205 | if entry == nil { | ||
| 207 | continue | 206 | continue | ||
| 208 | } | 207 | } | ||
| 209 | - | if canonicalHistoryDomain(entry.URL) == domain { | 208 | + | if browserurl.CanonicalDomain(entry.URL) == domain { |
| 210 | filtered = append(filtered, entry) | 209 | filtered = append(filtered, entry) | ||
| 211 | } | 210 | } | ||
| 212 | } | 211 | } | ||
| @@ -214,11 +213,7 @@ func filterHistoryEntriesByDomain(entries []*entity.HistoryEntry, domain string) | |||||
| 214 | } | 213 | } | ||
| 215 | 214 | ||||
| 216 | func displayHistoryDomain(raw string) string { | 215 | func displayHistoryDomain(raw string) string { | ||
| 217 | - | domain := canonicalHistoryDomain(raw) | 216 | + | return browserurl.DisplayDomain(raw) |
| 218 | - | if host, _, err := net.SplitHostPort(domain); err == nil { | |||
| 219 | - | domain = host | |||
| 220 | - | } | |||
| 221 | - | return domain | |||
| 222 | } | 217 | } | ||
| 223 | 218 | ||||
| 224 | func historyDomainActionKey(domain *entity.DomainStat) string { | 219 | func historyDomainActionKey(domain *entity.DomainStat) string { | ||
| @@ -235,27 +230,6 @@ func historyDomainDisplayLabel(domain *entity.DomainStat) string { | |||||
| 235 | return displayHistoryDomain(domain.Domain) | 230 | return displayHistoryDomain(domain.Domain) | ||
| 236 | } | 231 | } | ||
| 237 | 232 | ||||
| 238 | - | func canonicalHistoryDomain(raw string) string { | |||
| 239 | - | raw = strings.TrimSpace(strings.ToLower(raw)) | |||
| 240 | - | if raw == "" { | |||
| 241 | - | return "" | |||
| 242 | - | } | |||
| 243 | - | if parsed, err := url.Parse(raw); err == nil && parsed.Host != "" { | |||
| 244 | - | return strings.TrimPrefix(parsed.Host, "www.") | |||
| 245 | - | } | |||
| 246 | - | if schemeIdx := strings.Index(raw, "://"); schemeIdx >= 0 { | |||
| 247 | - | if parsed, err := url.Parse(raw[schemeIdx+3:]); err == nil && parsed.Host != "" { | |||
| 248 | - | return strings.TrimPrefix(parsed.Host, "www.") | |||
| 249 | - | } | |||
| 250 | - | } | |||
| 251 | - | for _, sep := range []string{"/", "?", "#"} { | |||
| 252 | - | if before, _, ok := strings.Cut(raw, sep); ok { | |||
| 253 | - | raw = before | |||
| 254 | - | } | |||
| 255 | - | } | |||
| 256 | - | return strings.TrimPrefix(raw, "www.") | |||
| 257 | - | } | |||
| 258 | - | ||||
| 259 | func formatHistoryTime(ts time.Time) string { | 233 | func formatHistoryTime(ts time.Time) string { | ||
| 260 | if ts.IsZero() { | 234 | if ts.IsZero() { | ||
| 261 | return "" | 235 | return "" | ||