diff --git a/browser.go b/browser.go index d7969d7..97b444c 100644 --- a/browser.go +++ b/browser.go @@ -7,9 +7,11 @@ import ( "fmt" "io" "io/ioutil" + "net/url" "os" "os/exec" "path/filepath" + "strings" ) // Stdout is the io.Writer to which executed commands write standard output. @@ -19,12 +21,34 @@ var Stdout io.Writer = os.Stdout var Stderr io.Writer = os.Stderr // OpenFile opens new browser window for the file path. +// A URL fragment after '#' (for example "index.html#section") is preserved. func OpenFile(path string) error { - path, err := filepath.Abs(path) + path, frag := splitFragment(path) + abs, err := filepath.Abs(path) if err != nil { return err } - return OpenURL("file://" + path) + return OpenURL(fileURL(abs, frag)) +} + +// splitFragment separates a filesystem path from an optional URL fragment. +func splitFragment(path string) (file, fragment string) { + i := strings.LastIndex(path, "#") + if i < 0 { + return path, "" + } + return path[:i], path[i+1:] +} + +// fileURL builds a file: URL for an absolute filesystem path, with optional fragment. +func fileURL(absPath, fragment string) string { + p := filepath.ToSlash(absPath) + if !strings.HasPrefix(p, "/") { + // Windows drive path, e.g. C:/foo → /C:/foo + p = "/" + p + } + u := url.URL{Scheme: "file", Path: p, Fragment: fragment} + return u.String() } // OpenReader consumes the contents of r and presents the diff --git a/browser_test.go b/browser_test.go new file mode 100644 index 0000000..be86525 --- /dev/null +++ b/browser_test.go @@ -0,0 +1,32 @@ +package browser + +import ( + "strings" + "testing" +) + +func TestSplitFragment(t *testing.T) { + file, frag := splitFragment(`c:\some-file.html#basic`) + if file != `c:\some-file.html` { + t.Fatalf("file = %q", file) + } + if frag != "basic" { + t.Fatalf("frag = %q", frag) + } + f2, g2 := splitFragment("index.html") + if f2 != "index.html" || g2 != "" { + t.Fatalf("got %q %q", f2, g2) + } +} + +func TestFileURLPreservesFragment(t *testing.T) { + got := fileURL("/tmp/doc.html", "basic") + want := "file:///tmp/doc.html#basic" + if got != want { + t.Fatalf("got %q want %q", got, want) + } + got = fileURL("C:/some-file.html", "basic") + if !strings.HasPrefix(got, "file:///C:/some-file.html") || !strings.HasSuffix(got, "#basic") { + t.Fatalf("windows-style got %q", got) + } +} diff --git a/browser_windows.go b/browser_windows.go index 63e1929..f4a3cad 100644 --- a/browser_windows.go +++ b/browser_windows.go @@ -1,7 +1,6 @@ package browser -import "golang.org/x/sys/windows" - +// Windows ShellExecute drops fragments on file: URLs. FileProtocolHandler keeps them. func openBrowser(url string) error { - return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(url), nil, nil, windows.SW_SHOWNORMAL) + return runCmd("rundll32", "url.dll,FileProtocolHandler", url) }