fix: normalize bare URLs in capture flow
This commit is contained in:
@@ -56,6 +56,11 @@ func (a *App) CaptureURLWithContext(rawURL, title, source, contextJSON string) (
|
||||
if rawURL == "" {
|
||||
return nil, fmt.Errorf("url required")
|
||||
}
|
||||
normalizedURL, ok := normalizeHTTPURL(rawURL)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid url")
|
||||
}
|
||||
rawURL = normalizedURL
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
title = linkTitle(rawURL, "")
|
||||
|
||||
@@ -35,8 +35,8 @@ func classifyClipboardText(text string) (string, string) {
|
||||
if value == "" {
|
||||
return "text", ""
|
||||
}
|
||||
if isURLLike(value) {
|
||||
return "url", value
|
||||
if normalized, ok := normalizeHTTPURL(value); ok {
|
||||
return "url", normalized
|
||||
}
|
||||
return "text", value
|
||||
}
|
||||
|
||||
@@ -118,6 +118,11 @@ func (a *App) createResolvedLink(nodeID, rawURL, title, note, source, capturedAt
|
||||
if rawURL == "" {
|
||||
return nil, fmt.Errorf("url required")
|
||||
}
|
||||
normalizedURL, ok := normalizeHTTPURL(rawURL)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid url")
|
||||
}
|
||||
rawURL = normalizedURL
|
||||
title = linkTitle(rawURL, title)
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
id := util.UUID7()
|
||||
@@ -154,26 +159,50 @@ func linkTitle(rawURL, title string) string {
|
||||
}
|
||||
|
||||
func isURLLike(text string) bool {
|
||||
_, ok := normalizeHTTPURL(text)
|
||||
return ok
|
||||
}
|
||||
|
||||
func normalizeHTTPURL(text string) (string, bool) {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
if strings.ContainsAny(text, " \t\r\n") || strings.Contains(text, "@") {
|
||||
return "", false
|
||||
}
|
||||
u, err := url.Parse(text)
|
||||
return err == nil && u.Scheme != "" && u.Host != ""
|
||||
if err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" {
|
||||
return text, true
|
||||
}
|
||||
if u != nil && u.Scheme != "" {
|
||||
return "", false
|
||||
}
|
||||
withScheme := "https://" + text
|
||||
u, err = url.Parse(withScheme)
|
||||
if err != nil || u.Host == "" {
|
||||
return "", false
|
||||
}
|
||||
host := u.Hostname()
|
||||
if host == "" || !strings.Contains(host, ".") {
|
||||
return "", false
|
||||
}
|
||||
return withScheme, true
|
||||
}
|
||||
|
||||
func openExternalURL(rawURL string) error {
|
||||
if !isURLLike(rawURL) {
|
||||
normalizedURL, ok := normalizeHTTPURL(rawURL)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid url")
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", rawURL)
|
||||
cmd = exec.Command("open", normalizedURL)
|
||||
case "windows":
|
||||
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", rawURL)
|
||||
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", normalizedURL)
|
||||
default:
|
||||
cmd = exec.Command("xdg-open", rawURL)
|
||||
cmd = exec.Command("xdg-open", normalizedURL)
|
||||
}
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
@@ -263,3 +263,41 @@ func TestClassifyClipboardTextRoutesURLBeforePlainText(t *testing.T) {
|
||||
t.Fatalf("value = %q, want trimmed text", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyClipboardTextTreatsBareDomainsAsURLs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{name: "apex domain", input: " mirv.top ", want: "https://mirv.top"},
|
||||
{name: "www domain", input: "www.example.com", want: "https://www.example.com"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
kind, value := classifyClipboardText(tt.input)
|
||||
if kind != "url" {
|
||||
t.Fatalf("kind = %q, want url", kind)
|
||||
}
|
||||
if value != tt.want {
|
||||
t.Fatalf("value = %q, want %q", value, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureURLNormalizesBareDomain(t *testing.T) {
|
||||
app, _ := setupTestApp(t)
|
||||
|
||||
dto, err := app.CaptureURL("mirv.top", "")
|
||||
if err != nil {
|
||||
t.Fatalf("CaptureURL: %v", err)
|
||||
}
|
||||
|
||||
if dto.URL != "https://mirv.top" {
|
||||
t.Fatalf("URL = %q, want https://mirv.top", dto.URL)
|
||||
}
|
||||
if dto.Hostname != "mirv.top" {
|
||||
t.Fatalf("Hostname = %q, want mirv.top", dto.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,8 +19,8 @@
|
||||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-DFvUQBl-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-DvvUc9rb.css">
|
||||
<script type="module" crossorigin src="/assets/main-D6zAtuqe.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CtRnbH6M.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user