1package usecase
2
3import (
4 "context"
5 stdurl "net/url"
6 "strings"
7
8 "github.com/bnema/dumber/internal/application/port"
9 domainurl "github.com/bnema/dumber/internal/domain/url"
10)
11
12// NavigationURLNormalizer normalizes user navigation input while keeping
13// filesystem probing behind an application port.
14type NavigationURLNormalizer struct {
15 localPaths port.LocalPathResolver
16}
17
18// NewNavigationURLNormalizer creates a navigation URL normalizer.
19func NewNavigationURLNormalizer(localPaths port.LocalPathResolver) *NavigationURLNormalizer {
20 return &NavigationURLNormalizer{localPaths: localPaths}
21}
22
23// Normalize converts existing local paths to file:// URLs, then falls back to
24// the pure domain URL normalizer for URL-like decisions.
25func (n *NavigationURLNormalizer) Normalize(ctx context.Context, input string) string {
26 if input == "" {
27 return ""
28 }
29 if n != nil && n.localPaths != nil && shouldProbeLocalPath(input) {
30 if absPath, ok, err := n.localPaths.ResolveExistingPath(ctx, input); err == nil && ok {
31 return (&stdurl.URL{Scheme: "file", Path: absPath}).String()
32 }
33 }
34 return domainurl.Normalize(input)
35}
36
37// BuildNavigationURL resolves local paths before applying bang shortcuts and search fallback.
38func (n *NavigationURLNormalizer) BuildNavigationURL(
39 ctx context.Context,
40 input string,
41 shortcutURLs map[string]string,
42 defaultSearch string,
43) string {
44 return BuildNavigationURL(ctx, input, n.normalize, shortcutURLs, defaultSearch)
45}
46
47// BuildNavigationURL resolves a user navigation string using the shared CLI/UI policy.
48func BuildNavigationURL(
49 ctx context.Context,
50 input string,
51 normalize func(context.Context, string) string,
52 shortcutURLs map[string]string,
53 defaultSearch string,
54) string {
55 if _, _, found := domainurl.ParseBangShortcut(input); !found && normalize != nil {
56 normalized := normalize(ctx, input)
57 if normalized != input {
58 return normalized
59 }
60 }
61 return domainurl.BuildSearchURL(input, shortcutURLs, defaultSearch)
62}
63
64func (n *NavigationURLNormalizer) normalize(ctx context.Context, input string) string {
65 if n == nil {
66 return domainurl.Normalize(input)
67 }
68 return n.Normalize(ctx, input)
69}
70
71func shouldProbeLocalPath(input string) bool {
72 if input == "" {
73 return false
74 }
75 if hasPreservedNavigationScheme(input) {
76 return false
77 }
78 return true
79}
80
81func hasPreservedNavigationScheme(input string) bool {
82 parsed, err := stdurl.Parse(input)
83 if err != nil || parsed.Scheme == "" {
84 return false
85 }
86
87 scheme := strings.ToLower(parsed.Scheme)
88 lowerInput := strings.ToLower(input)
89
90 switch scheme {
91 case "http", "https", "dumb", "file", "vscode", "vscode-insiders", "spotify", "steam":
92 return strings.HasPrefix(lowerInput, scheme+"://") && domainurl.Normalize(input) == input
93 case "blob":
94 return strings.HasPrefix(lowerInput, "blob:") && strings.Contains(lowerInput, "://") &&
95 domainurl.Normalize(input) == input
96 case "about", "data", "javascript", "mailto":
97 return strings.HasPrefix(lowerInput, scheme+":") && domainurl.Normalize(input) == input
98 default:
99 return false
100 }
101}