diff --git a/assets/search/actions.js b/assets/search/actions.js index 97fcf10..aa5c65d 100644 --- a/assets/search/actions.js +++ b/assets/search/actions.js @@ -11,9 +11,58 @@ function rebuildIndex() { }); } +function encodeSearchPath(p) { + if (p === '/' || p === '') return '/'; + return '/' + p.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/'); +} + +// createSearchPage runs the new-page flow (pick a parent folder, then confirm +// the name) starting from the search query, so a query with no exact page can +// be turned into that page in one step. The name field is pre-filled with the +// query but stays editable. +function createSearchPage(name) { + var current = decodeURIComponent(window.location.pathname).replace(/\/+$/, '') || '/'; + openTreePicker({ + title: 'New page — where?', + mode: 'folder', + initialPath: current, + preselect: current, + hideFiles: true, + confirmLabel: 'NEXT', + onSelect: function (parentPath) { + var input = document.createElement('input'); + input.type = 'text'; + input.className = 'input'; + input.placeholder = 'Page name'; + input.value = name || ''; + openModal({ + title: 'New page — name?', + body: input, + confirm: { + label: 'CREATE', + onConfirm: function () { + var finalName = input.value.trim(); + if (!finalName) return; + var base = parentPath === '/' ? '/' : encodeSearchPath(parentPath) + '/'; + window.location.href = base + encodeURIComponent(finalName) + '/?edit'; + } + } + }); + } + }); +} + document.addEventListener('DOMContentLoaded', function () { wireDropdown(document.querySelector('[data-action="actions-drop"]')); + var createLink = document.querySelector('[data-create-page]'); + if (createLink) { + createLink.addEventListener('click', function (e) { + e.preventDefault(); + createSearchPage(createLink.getAttribute('data-create-page')); + }); + } + // Focus the search input on results pages so Tab steps directly into the // first match — the input sits immediately before the results in DOM // order, so the natural tab sequence is input → first result → next, … diff --git a/assets/search/main.html b/assets/search/main.html index ab5d81b..278f633 100644 --- a/assets/search/main.html +++ b/assets/search/main.html @@ -4,7 +4,17 @@ {{define "content"}} {{if .Query}} - {{if or .Pages .Files}} + {{if .Exact}} +

Exact Match

+ {{range .Exact}} +
+ {{.Name}} +
/{{.Path}}
+
+ {{end}} + {{else}} +

No page named “{{.Query}}” —

+ {{end}} {{if .Pages}}

Matching Pages {{.PageTotal}}

{{range .Pages}} @@ -27,9 +37,6 @@ {{end}} {{end}} - {{else}} -

No matches for “{{.Query}}”.

- {{end}} {{else}}

Enter a query above.

{{end}} diff --git a/search.go b/search.go index 20d8cab..31bc7f0 100644 --- a/search.go +++ b/search.go @@ -18,6 +18,10 @@ import ( // section header still reports the true total so the user knows more exist. const searchSectionCap = 10 +// exactNameScore is the score scoreName assigns to a whole-name exact match. +// handleSearch peels these into the promoted "Exact Match" section. +const exactNameScore = 1000 + type searchResult struct { Name string URL string @@ -32,8 +36,11 @@ type searchPageData struct { Title string EditMode bool Query string - Pages []searchResult - Files []searchResult + // Exact holds page(s) whose name equals the query exactly, promoted to + // their own section above the fuzzy matches. + Exact []searchResult + Pages []searchResult + Files []searchResult // PageTotal/FileTotal are the true match totals; Pages/Files are capped at // searchSectionCap for rendering. PageTotal int @@ -101,6 +108,15 @@ func (h *handler) handleSearch(w http.ResponseWriter, r *http.Request) { pages, builtAt := searchWiki(query) files := searchFiles(query) + // searchWiki sorts by score desc, so exact-name matches (score 1000) are + // at the front; peel them into their own section and drop them from the + // fuzzy page list so they aren't shown twice. + var exact []searchResult + for len(pages) > 0 && pages[0].Score == exactNameScore { + exact = append(exact, pages[0]) + pages = pages[1:] + } + title := "Search" if query != "" { title = "Search: " + query @@ -108,6 +124,7 @@ func (h *handler) handleSearch(w http.ResponseWriter, r *http.Request) { data := searchPageData{ Title: title, Query: query, + Exact: exact, Pages: capResults(pages), Files: capResults(files), PageTotal: len(pages), @@ -242,7 +259,7 @@ func fileURL(relPath string) string { // large file volumes don't pay the edit-distance cost per query. func scoreName(nameLower string, nameTokens []string, qLower string, qTokens []string, fuzzy bool) int { if nameLower == qLower { - return 1000 + return exactNameScore } score := 0 for _, qt := range qTokens {