261029.4 · 00011b6688dc6372821c09b0a62727dbb8effcb3
A search's recent pages come only from the page's own tree
Recent pages crossed trees, so on the web address a search in one repo listed pages from repos read before. Each page's search box now names its tree, and each stored entry keeps the tree it was opened in. A version counts as its tree. Entries stored before this have no tree and are dropped.
reading declarations
@@ -13,7 +13,7 @@ <nav>{{range $i, $c := .Crumbs}}{{if $i}} / {{end}}{{if $c.Href}}<a href="{{$c.Href}}"{{with $c.Title}} title="{{.}}"{{end}}>{{$c.Label}}</a>{{else if $c.Title}}<span title="{{$c.Title}}">{{$c.Label}}</span>{{else}}<span>{{$c.Label}}</span>{{end}}{{end}}</nav> {{if not .Stopped}}<span>{{with .Status}}<span class="age">{{.}}</span>{{end}}{{if .Raw}}<a href="?raw">raw</a>{{end}}{{if not (or .Shared .NoStop)}}<form method="post" action="{{ownPath}}stop">{{if .Tree}}<input type="hidden" name="tree" value="{{.Tree}}"><button>Delete lock</button>{{else}}<button class="kill">Kill server</button>{{end}}</form>{{end}}</span>{{end}} </header>-{{if not .Stopped}}{{with .Search}}<div class="search"><input type="search" class="search" data-search="{{.}}" placeholder="search" aria-label="Search this directory" autocomplete="off"><span class="tools"><button class="copy" title="copy the page's text" aria-label="copy the page's text">⧉</button>{{if $.Forge}}<button class="share" data-forge="{{$.Forge}}" data-lines="{{$.ForgeLines}}" title="copy the forge's link to what is selected, or to the page" aria-label="copy the forge's link to what is selected, or to the page">⤴</button>{{else}}<button class="share" title="copy a link to what is selected, or to the page" aria-label="copy a link to what is selected, or to the page">⤴</button>{{end}}</span></div>+{{if not .Stopped}}{{with .Search}}<div class="search"><input type="search" class="search" data-search="{{.}}" data-recents="{{$.Recents}}" placeholder="search" aria-label="Search this directory" autocomplete="off"><span class="tools"><button class="copy" title="copy the page's text" aria-label="copy the page's text">⧉</button>{{if $.Forge}}<button class="share" data-forge="{{$.Forge}}" data-lines="{{$.ForgeLines}}" title="copy the forge's link to what is selected, or to the page" aria-label="copy the forge's link to what is selected, or to the page">⤴</button>{{else}}<button class="share" title="copy a link to what is selected, or to the page" aria-label="copy a link to what is selected, or to the page">⤴</button>{{end}}</span></div> {{end}}{{end}}<main> {{.Body}} </main>
@@ -578,12 +578,19 @@ // origin, never on the server, so each viewer of a share sees only // what that viewer opened. Each entry counts its visits and keeps the // last; the score weighs both, as zoxide does. A reload is no visit.+ // Each entry names its tree, and a search lists only the page's own+ // tree, so one repo's reading never shows in another's. An entry+ // without a tree predates that and is dropped. const recentKey = "recent"; const maxRecent = 300;+ const recentTree = () => {+ const b = box();+ return b ? b.dataset.recents : undefined;+ }; const readRecent = () => { try { const list = JSON.parse(localStorage.getItem(recentKey));- return Array.isArray(list) ? list : [];+ return Array.isArray(list) ? list.filter((e) => typeof e.r === "string") : []; } catch { return []; }@@ -594,7 +601,9 @@ }; const recentPaths = () => { const now = Date.now();+ const tree = recentTree(); return readRecent()+ .filter((e) => e.r === tree) .sort((a, b) => score(b, now) - score(a, now)) .map((e) => e.p); };@@ -617,13 +626,16 @@ if (!["doc", "code", "dir"].includes(document.body.className)) return; const nav = performance.getEntriesByType("navigation")[0]; if (nav && nav.type === "reload") return;+ const tree = recentTree();+ if (tree === undefined) return; const now = Date.now(); const list = readRecent(); const e = list.find((e) => e.p === location.pathname); if (e) { e.n++; e.t = now;- } else list.push({ p: location.pathname, n: 1, t: now });+ e.r = tree;+ } else list.push({ p: location.pathname, r: tree, n: 1, t: now }); list.sort((a, b) => score(b, now) - score(a, now)); try { localStorage.setItem(recentKey, JSON.stringify(list.slice(0, maxRecent)));
@@ -40,7 +40,7 @@ The list arrives after the page, since the server walks the tree for it: a few m ## Recent pages -The browser remembers the pages you open; the server keeps no such list. A search shows the best five that match its words first, under recent, from any tree on the address.+The browser remembers the pages you open; the server keeps no such list. A search shows the best five that match its words first, under recent. They come from the tree you are in, so one repo's reading never shows in another's. A version counts as its tree. Each visit counts and recent visits weigh more, as in zoxide. A reload is no visit.
@@ -1,6 +1,7 @@ package main import (+ "context" "encoding/json" "net/url" "os"@@ -59,6 +60,31 @@ func TestSearchBoxScope(t *testing.T) { } } +// Recent pages stay within their tree: each page names its tree for the+// script, a version names the tree it is a version of, and a tree at /+// names itself, not the trees whose paths lie below it.+func TestRecentsStayInTheirTree(t *testing.T) {+ s, dir := repoTree(t)+ tr := s.list()[0]+ tr.prefix, tr.label = "/notes/", "~/notes"+ other := t.TempDir()+ os.WriteFile(filepath.Join(other, "x.md"), []byte("# x\n"), 0o644)+ addTree(t, s, other, "/", "~")+ h := s.handler()+ star := stardatesIn(context.Background(), dir).byHash[head(t, dir, "HEAD~1")]+ for _, c := range []struct{ url, want string }{+ {"/notes/", "/notes/"},+ {"/notes/a/b.go", "/notes/"},+ {"/notes@" + star + "/a/b.go", "/notes/"},+ {"/x.md", "/"},+ } {+ w := get(h, c.url)+ if !strings.Contains(w.Body.String(), `data-recents="`+c.want+`"`) {+ t.Errorf("%s: want data-recents=%q, status %d", c.url, c.want, w.Code)+ }+ }+}+ // A directory's own entries come first and are marked here; deeper ones // follow unmarked. func TestSearchHereFirst(t *testing.T) {
@@ -1251,6 +1251,7 @@ type page struct { Raw bool Tree string // prefix the stop button withdraws, or "" for every tree Search string // the tree's URL, which answers ?search=, or "" off a tree+ Recents string // the tree whose pages a search lists as recent; a version's is its tree's Status template.HTML // the server's age, on the index, or a repo's commit on the web address Style template.CSS // the stylesheet inline, for a page the server may not outlive Stopped bool@@ -1346,8 +1347,14 @@ func render(w http.ResponseWriter, r *http.Request, status int, p page) { // shared, and a search box for the directory the page is in, with a button // that copies the page's path on disk. A failed page searches the whole // tree, since its directory may not exist, and has no path to copy.+// Recent pages stay within the tree, so one repo's reading never shows in+// another's search. func (t *tree) render(w http.ResponseWriter, r *http.Request, status int, p page) { p.Tree, p.Shared, p.Search = t.prefix, t.shared, t.prefixURL()+ p.Recents = t.prefixURL()+ if t.base != nil {+ p.Recents = t.base.prefixURL()+ } if t.web != nil { p.NoStop, p.Status = true, t.web.status(t.prefix, r.URL.Path) }