commit 5d08390cffcc3fcbf3bc98235ee0091f1b5d3075 equwal <truex@equwal.com> 2026-09-22 19:26:18 -0700 Search the bookmarks from the address bar of the browser Each page links to an OpenSearch description at /opensearch.xml, so a browser can add the search of the bookmarks. The bookmarks page also tells the address to add by hand, with the shortcut sbm. Then "sbm" and words in the address bar open the search on the bookmarks page.
README.md | 4 ++++ bookmarks.go | 14 ++++++++++++++ main.go | 1 + pages.html | 10 ++++++++++ web_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+)
diff --git a/README.md b/README.md index 5d8669d..3ace8e7 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ page does what bm does: tab), as in the add-on. Pick a tag. Sort by date, URL, description or tag. Text that no bookmark matches opens as an address, or as a web search. +- Search from the address bar of the browser: type sbm, a space and your + words. Pages link to an OpenSearch description, `/opensearch.xml`, and + the bookmarks page tells the address to add: + `https://sbm.subread.space/bookmarks?q=%s`. - Add a bookmark. A URL that is a bookmark already is refused. - Edit or delete a bookmark. - Edit the whole file, as `bm -e` does. diff --git a/bookmarks.go b/bookmarks.go index 7f25370..ea867e6 100644 --- a/bookmarks.go +++ b/bookmarks.go @@ -747,3 +747,17 @@ func (s *server) script(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/javascript; charset=utf-8") io.WriteString(w, searchJS) } + +// openSearch describes the search of the bookmarks in the OpenSearch 1.1 +// format, so that a browser can search them from its address bar. +func (s *server) openSearch(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/opensearchdescription+xml") + fmt.Fprintf(w, `<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> +<ShortName>sbm</ShortName> +<Description>Search your sbm bookmarks</Description> +<InputEncoding>UTF-8</InputEncoding> +<Url type="text/html" method="get" template="%s/bookmarks?q={searchTerms}"/> +</OpenSearchDescription> +`, html.EscapeString(s.site)) +} diff --git a/main.go b/main.go index 2002511..261ec2d 100644 --- a/main.go +++ b/main.go @@ -179,6 +179,7 @@ func (s *server) routes() http.Handler { m.HandleFunc("POST /bookmarks/import", s.importFile) m.HandleFunc("GET /bookmarks.txt", s.download) m.HandleFunc("GET /search.js", s.script) + m.HandleFunc("GET /opensearch.xml", s.openSearch) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := w.Header() // Scripts only from this server: the live search of the bookmark diff --git a/pages.html b/pages.html index 45f357e..9a2e152 100644 --- a/pages.html +++ b/pages.html @@ -12,6 +12,7 @@ <meta property="og:image" content="https://sbm.subread.space/og.png"> <meta name="twitter:card" content="summary_large_image"> <meta name="theme-color" content="#005577"> +<link rel="search" type="application/opensearchdescription+xml" title="sbm bookmarks" href="/opensearch.xml"> <style> body { max-width: 36em; margin: 2em auto; padding: 0 1em; font: 16px/1.5 sans-serif; color: #222; background: #fff; } a { color: #005577; } @@ -164,6 +165,15 @@ works too. URLs that are bookmarks already are skipped.</p> <label>File <input type="file" name="file" required></label> <button>Import</button> </form></details> +<details><summary>Search from the address bar</summary> +<p class="note">Add a search engine to your browser with this address, and +give it the shortcut sbm:</p> +<p><code>{{.URL}}/bookmarks?q=%s</code></p> +<p class="note">Then type sbm, a space and your words in the address bar. +In Chrome and Edge, add it under Settings, Search engine, Manage search +engines and site search. Firefox offers to add sbm from the search button of +the address bar while this page is open.</p> +</details> <div id="results"> <p class="note">{{.Found}} of {{.Total}} bookmarks{{with .Tag}} with the tag <b>{{.}}</b>{{end}}.{{if gt .Found (len .Rows)}} This page shows the first {{len .Rows}}.{{end}}{{if or .Query .Tag}} <a href="/bookmarks">Show all</a>{{end}}</p> diff --git a/web_test.go b/web_test.go index 29b1ec4..47da09f 100644 --- a/web_test.go +++ b/web_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/xml" "io" "mime/multipart" "net/http" @@ -433,6 +434,43 @@ func TestOtherSiteCannotChangeBookmarks(t *testing.T) { } } +func TestSearchFromTheAddressBar(t *testing.T) { + e := newEnv(t, false) + resp, err := http.Get(e.web.URL + "/opensearch.xml") + if err != nil { + t.Fatal(err) + } + b, _ := io.ReadAll(resp.Body) + resp.Body.Close() + var d struct { + ShortName string + URL struct { + Type string `xml:"type,attr"` + Template string `xml:"template,attr"` + } `xml:"Url"` + } + if err := xml.Unmarshal(b, &d); err != nil { + t.Fatalf("the description is not XML: %v\n%s", err, b) + } + if resp.Header.Get("Content-Type") != "application/opensearchdescription+xml" || d.ShortName != "sbm" || + d.URL.Type != "text/html" || d.URL.Template != e.web.URL+"/bookmarks?q={searchTerms}" { + t.Errorf("description: %q %+v", resp.Header.Get("Content-Type"), d) + } + // Each page tells the browser where the description is. + if !strings.Contains(e.body(e.browser(), "/"), `<link rel="search" type="application/opensearchdescription+xml" title="sbm bookmarks" href="/opensearch.xml">`) { + t.Error("the home page does not link to the description") + } + // The bookmarks page tells the address for a browser, and that address + // gives the search. + c, _, _ := e.account("https://a.org\tA\t\nhttps://b.org\tB\t\n") + if !strings.Contains(e.body(c, "/bookmarks"), "<code>"+e.web.URL+"/bookmarks?q=%s</code>") { + t.Error("the bookmarks page does not tell the address for the browser") + } + if got := titles(e.body(c, "/bookmarks?q=b.org")); !slices.Equal(got, []string{"B"}) { + t.Errorf("search from the address bar: %q", got) + } +} + func TestSearchOnTheHomePage(t *testing.T) { e := newEnv(t, false) e.withMail()