fix: normalize bare URLs in capture flow

This commit is contained in:
2026-06-05 12:29:19 +08:00
parent 56ef211418
commit 6d15639b41
11 changed files with 139 additions and 27 deletions
+5
View File
@@ -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, "")
+2 -2
View File
@@ -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
}
+35 -6
View File
@@ -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()
}
+38
View File
@@ -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
+2 -2
View File
@@ -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>