Add a search page to the docs.

Now instead of clicking the "Google Site Search"' button, the user has the option
of clicking the "Search" button, which will take them to a results page. Currently,
the results are identical to the autocomplete results, but the number of results
is not limited to 30.  In the future, more information and search options could
be added to this page to make a more powerful search.

Fixes #107
main
Jeremy Salwen 2023-01-25 17:57:10 -05:00 committed by Henrik Böving
parent 7b0ebfa527
commit 033003c6cb
8 changed files with 88 additions and 30 deletions

View File

@ -12,6 +12,7 @@ import DocGen4.Output.Module
import DocGen4.Output.NotFound import DocGen4.Output.NotFound
import DocGen4.Output.Find import DocGen4.Output.Find
import DocGen4.Output.SourceLinker import DocGen4.Output.SourceLinker
import DocGen4.Output.Search
import DocGen4.Output.ToJson import DocGen4.Output.ToJson
import DocGen4.Output.FoundationalTypes import DocGen4.Output.FoundationalTypes
import DocGen4.LeanInk.Process import DocGen4.LeanInk.Process
@ -35,11 +36,13 @@ def htmlOutputSetup (config : SiteBaseContext) : IO Unit := do
let notFoundHtml := ReaderT.run notFound config |>.toString let notFoundHtml := ReaderT.run notFound config |>.toString
let foundationalTypesHtml := ReaderT.run foundationalTypes config |>.toString let foundationalTypesHtml := ReaderT.run foundationalTypes config |>.toString
let navbarHtml := ReaderT.run navbar config |>.toString let navbarHtml := ReaderT.run navbar config |>.toString
let searchHtml := ReaderT.run search config |>.toString
let docGenStatic := #[ let docGenStatic := #[
("style.css", styleCss), ("style.css", styleCss),
("declaration-data.js", declarationDataCenterJs), ("declaration-data.js", declarationDataCenterJs),
("nav.js", navJs), ("nav.js", navJs),
("how-about.js", howAboutJs), ("how-about.js", howAboutJs),
("search.html", searchHtml),
("search.js", searchJs), ("search.js", searchJs),
("mathjax-config.js", mathjaxConfigJs), ("mathjax-config.js", mathjaxConfigJs),
("instances.js", instancesJs), ("instances.js", instancesJs),

View File

@ -0,0 +1,28 @@
/-
Copyright (c) 2023 Jeremy Salwen. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Jeremy Salwen
-/
import DocGen4.Output.ToHtmlFormat
import DocGen4.Output.Template
namespace DocGen4
namespace Output
open scoped DocGen4.Jsx
def search : BaseHtmlM Html := do templateExtends (baseHtml "Search") <|
pure <|
<main>
<a id="top"></a>
<h1> Search Results </h1>
<label for="search_page_query">Query:</label><input id="search_page_query" />
<script>
document.getElementById("search_page_query").value = new URL(window.location.href).searchParams.get("q")
</script>
<div id="search_results">
</div>
</main>
end Output
end DocGen4

View File

@ -45,10 +45,10 @@ def baseHtmlGenerator (title : String) (site : Array Html) : BaseHtmlM Html := d
<header> <header>
<h1><label for="nav_toggle"></label>Documentation</h1> <h1><label for="nav_toggle"></label>Documentation</h1>
<p class="header_filename break_within">{title}</p> <p class="header_filename break_within">{title}</p>
-- TODO: Replace this form with our own search
<form action="https://google.com/search" method="get" id="search_form"> <form action="https://google.com/search" method="get" id="search_form">
<input type="hidden" name="sitesearch" value="https://leanprover-community.github.io/mathlib4_docs"/> <input type="hidden" name="sitesearch" value="https://leanprover-community.github.io/mathlib4_docs"/>
<input type="text" name="q" autocomplete="off"/>&#32; <input type="text" name="q" autocomplete="off"/>&#32;
<button onclick="javascript: form.action='/search.html';">Search</button>
<button>Google site search</button> <button>Google site search</button>
</form> </form>
</header> </header>

View File

@ -89,6 +89,7 @@ def baseDirBlackList : HashSet String :=
"find", "find",
"how-about.js", "how-about.js",
"index.html", "index.html",
"search.html",
"foundational_types.html", "foundational_types.html",
"mathjax-config.js", "mathjax-config.js",
"navbar.html", "navbar.html",

View File

@ -94,6 +94,7 @@ library_facet docs (lib) : FilePath := do
basePath / "index.html", basePath / "index.html",
basePath / "404.html", basePath / "404.html",
basePath / "navbar.html", basePath / "navbar.html",
basePath / "search.html",
basePath / "find" / "index.html", basePath / "find" / "index.html",
basePath / "find" / "find.js", basePath / "find" / "find.js",
basePath / "src" / "alectryon.css", basePath / "src" / "alectryon.css",

View File

@ -67,7 +67,7 @@ export class DeclarationDataCenter {
* Search for a declaration. * Search for a declaration.
* @returns {Array<any>} * @returns {Array<any>}
*/ */
search(pattern, strict = true) { search(pattern, strict = true, maxResults=undefined) {
if (!pattern) { if (!pattern) {
return []; return [];
} }
@ -75,7 +75,7 @@ export class DeclarationDataCenter {
let decl = this.declarationData.declarations[pattern]; let decl = this.declarationData.declarations[pattern];
return decl ? [decl] : []; return decl ? [decl] : [];
} else { } else {
return getMatches(this.declarationData.declarations, pattern); return getMatches(this.declarationData.declarations, pattern, maxResults);
} }
} }
@ -159,7 +159,7 @@ function matchCaseSensitive(declName, lowerDeclName, pattern) {
} }
} }
function getMatches(declarations, pattern, maxResults = 30) { function getMatches(declarations, pattern, maxResults = undefined) {
const lowerPats = pattern.toLowerCase().split(/\s/g); const lowerPats = pattern.toLowerCase().split(/\s/g);
const patNoSpaces = pattern.replace(/\s/g, ""); const patNoSpaces = pattern.replace(/\s/g, "");
const results = []; const results = [];

View File

@ -4,27 +4,36 @@
import { DeclarationDataCenter } from "./declaration-data.js"; import { DeclarationDataCenter } from "./declaration-data.js";
// Search form and input in the upper right toolbar
const SEARCH_FORM = document.querySelector("#search_form"); const SEARCH_FORM = document.querySelector("#search_form");
const SEARCH_INPUT = SEARCH_FORM.querySelector("input[name=q]"); const SEARCH_INPUT = SEARCH_FORM.querySelector("input[name=q]");
// Create an `div#search_results` to hold all search results. // Search form on the /search.html_page. These may be null.
let sr = document.createElement("div"); const SEARCH_PAGE_INPUT = document.querySelector("#search_page_query")
sr.id = "search_results"; const SEARCH_RESULTS = document.querySelector("#search_results")
SEARCH_FORM.appendChild(sr);
// Max results to show for autocomplete or /search.html page.
const AC_MAX_RESULTS = 30
const SEARCH_PAGE_MAX_RESULTS = undefined
// Create an `div#autocomplete_results` to hold all autocomplete results.
let ac_results = document.createElement("div");
ac_results.id = "autocomplete_results";
SEARCH_FORM.appendChild(ac_results);
/** /**
* Attach `selected` class to the the selected search result. * Attach `selected` class to the the selected autocomplete result.
*/ */
function handleSearchCursorUpDown(down) { function handleSearchCursorUpDown(down) {
const sel = sr.querySelector(`.selected`); const sel = ac_results.querySelector(`.selected`);
if (sel) { if (sel) {
sel.classList.remove("selected"); sel.classList.remove("selected");
const toSelect = down const toSelect = down
? sel.nextSibling || sr.firstChild ? sel.nextSibling || ac_results.firstChild
: sel.previousSibling || sr.lastChild; : sel.previousSibling || ac_results.lastChild;
toSelect && toSelect.classList.add("selected"); toSelect && toSelect.classList.add("selected");
} else { } else {
const toSelect = down ? sr.firstChild : sr.lastChild; const toSelect = down ? ac_results.firstChild : ac_results.lastChild;
toSelect && toSelect.classList.add("selected"); toSelect && toSelect.classList.add("selected");
} }
} }
@ -33,12 +42,12 @@ function handleSearchCursorUpDown(down) {
* Perform search (when enter is pressed). * Perform search (when enter is pressed).
*/ */
function handleSearchEnter() { function handleSearchEnter() {
const sel = sr.querySelector(`.selected`) || sr.firstChild; const sel = ac_results.querySelector(`.selected`) || ac_results.firstChild;
sel.click(); sel.click();
} }
/** /**
* Allow user to navigate search results with up/down arrow keys, and choose with enter. * Allow user to navigate autocomplete results with up/down arrow keys, and choose with enter.
*/ */
SEARCH_INPUT.addEventListener("keydown", (ev) => { SEARCH_INPUT.addEventListener("keydown", (ev) => {
switch (ev.key) { switch (ev.key) {
@ -71,7 +80,7 @@ function removeAllChildren(node) {
/** /**
* Handle user input and perform search. * Handle user input and perform search.
*/ */
function handleSearch(dataCenter, err, ev) { function handleSearch(dataCenter, err, ev, sr, maxResults) {
const text = ev.target.value; const text = ev.target.value;
// If no input clear all. // If no input clear all.
@ -85,12 +94,12 @@ function handleSearch(dataCenter, err, ev) {
sr.setAttribute("state", "loading"); sr.setAttribute("state", "loading");
if (dataCenter) { if (dataCenter) {
const result = dataCenter.search(text, false); const result = dataCenter.search(text, false, maxResults);
// in case user has updated the input. // in case user has updated the input.
if (ev.target.value != text) return; if (ev.target.value != text) return;
// update search results // update autocomplete results
removeAllChildren(sr); removeAllChildren(sr);
for (const { name, docLink } of result) { for (const { name, docLink } of result) {
const d = sr.appendChild(document.createElement("a")); const d = sr.appendChild(document.createElement("a"));
@ -111,8 +120,15 @@ function handleSearch(dataCenter, err, ev) {
DeclarationDataCenter.init() DeclarationDataCenter.init()
.then((dataCenter) => { .then((dataCenter) => {
// Search autocompletion. // Search autocompletion.
SEARCH_INPUT.addEventListener("input", ev => handleSearch(dataCenter, null, ev)); SEARCH_INPUT.addEventListener("input", ev => handleSearch(dataCenter, null, ev, ac_results, AC_MAX_RESULTS));
if(SEARCH_PAGE_INPUT) {
SEARCH_PAGE_INPUT.addEventListener("input", ev => handleSearch(dataCenter, null, ev, SEARCH_RESULTS, SEARCH_PAGE_MAX_RESULTS))
SEARCH_PAGE_INPUT.dispatchEvent(new Event("input"))
}
}) })
.catch(e => { .catch(e => {
SEARCH_INPUT.addEventListener("input", ev => handleSearch(null, e, ev)); SEARCH_INPUT.addEventListener("input", ev => handleSearch(null, e, ev, ac_results, AC_MAX_RESULTS));
if(SEARCH_PAGE_INPUT) {
SEARCH_PAGE_INPUT.addEventListener("input", ev => handleSearch(null, e, ev, SEARCH_RESULTS, SEARCH_PAGE_MAX_RESULTS));
}
}); });

View File

@ -116,7 +116,7 @@ header {
:root { --header-side-padding: 1ex; } :root { --header-side-padding: 1ex; }
#search_form button { display: none; } #search_form button { display: none; }
#search_form input { width: 100%; } #search_form input { width: 100%; }
header #search_results { header #autocomplete_results {
left: 1ex; left: 1ex;
right: 1ex; right: 1ex;
width: inherit; width: inherit;
@ -146,7 +146,7 @@ header header_filename {
} }
/* inserted by nav.js */ /* inserted by nav.js */
#search_results { #autocomplete_results {
position: absolute; position: absolute;
top: var(--header-height); top: var(--header-height);
right: calc(var(--header-side-padding)); right: calc(var(--header-side-padding));
@ -160,15 +160,15 @@ header header_filename {
max-height: calc(100vh - var(--header-height)); max-height: calc(100vh - var(--header-height));
} }
#search_results:empty { #autocomplete_results:empty {
display: none; display: none;
} }
#search_results[state="loading"]:empty { #autocomplete_results[state="loading"]:empty {
display: block; display: block;
cursor: progress; cursor: progress;
} }
#search_results[state="loading"]:empty::before { #autocomplete_results[state="loading"]:empty::before {
display: block; display: block;
content: ' 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 '; content: ' 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 ';
padding: 1ex; padding: 1ex;
@ -179,17 +179,17 @@ header header_filename {
100% { transform: translate(-100%, 0); } 100% { transform: translate(-100%, 0); }
} }
#search_results[state="done"]:empty { #autocomplete_results[state="done"]:empty {
display: block; display: block;
text-align: center; text-align: center;
padding: 1ex; padding: 1ex;
} }
#search_results[state="done"]:empty::before { #autocomplete_results[state="done"]:empty::before {
content: '(no results)'; content: '(no results)';
font-style: italic; font-style: italic;
} }
#search_results a { #autocomplete_results a {
display: block; display: block;
color: inherit; color: inherit;
padding: 1ex; padding: 1ex;
@ -197,11 +197,20 @@ header header_filename {
padding-left: 0.5ex; padding-left: 0.5ex;
cursor: pointer; cursor: pointer;
} }
#search_results .selected { #autocomplete_results .selected {
background: white; background: white;
border-color: #f0a202; border-color: #f0a202;
} }
#search_results a {
display: block;
}
#search_results[state="done"]:empty::before {
content: '(no results)';
font-style: italic;
}
main, nav { main, nav {
margin-top: calc(var(--header-height) + 1em); margin-top: calc(var(--header-height) + 1em);
} }
@ -476,7 +485,7 @@ main h2, main h3, main h4, main h5, main h6 {
} }
.imports li, code, .decl_header, .attributes, .structure_field_info, .imports li, code, .decl_header, .attributes, .structure_field_info,
.constructor, .instances li, .equation, #search_results div, .constructor, .instances li, .equation, #autocomplete_results div,
.structure_ext_ctor { .structure_ext_ctor {
font-family: 'Source Code Pro', monospace; font-family: 'Source Code Pro', monospace;
} }