Added search by signature in docs

This commit is contained in:
John Konecny 2024-10-25 07:25:14 -04:00
parent 8903d63cbf
commit 3974c9a978
No known key found for this signature in database
GPG key ID: E2BE1C73CA4FE2E7
4 changed files with 243 additions and 77 deletions

View file

@ -112,6 +112,10 @@ pub fn generate_docs_html(root_file: PathBuf, build_dir: &Path) {
.replace(
"<!-- Module links -->",
render_sidebar(exposed_module_docs.iter().map(|(_, docs)| docs)).as_str(),
)
.replace(
"<!-- Search Type Ahead -->",
render_search_type_ahead(exposed_module_docs.iter().map(|(_, docs)| docs)).as_str(),
);
let all_exposed_symbols = {
@ -469,6 +473,72 @@ fn render_sidebar<'a, I: Iterator<Item = &'a ModuleDocumentation>>(modules: I) -
buf
}
fn render_search_type_ahead<'a, I: Iterator<Item = &'a ModuleDocumentation>>(modules: I) -> String {
let mut buf = String::new();
for module in modules {
let module_name = module.name.as_str();
for entry in &module.entries {
if let DocEntry::DocDef(doc_def) = entry {
if module.exposed_symbols.contains(&doc_def.symbol) {
let mut entry_contents_buf = String::new();
push_html(
&mut entry_contents_buf,
"p",
vec![("class", "type-ahead-def-name")],
doc_def.name.as_str(),
);
let mut entry_signature_buf = String::new();
type_annotation_to_html(
0,
&mut entry_signature_buf,
&doc_def.type_annotation,
false,
);
push_html(
&mut entry_contents_buf,
"p",
vec![("class", "type-ahead-signature")],
entry_signature_buf.as_str(),
);
push_html(
&mut entry_contents_buf,
"p",
vec![("class", "type-ahead-doc-path")],
format!("{} > {}", module_name, doc_def.name),
);
let mut entry_href = String::new();
entry_href.push_str(module_name);
entry_href.push('#');
entry_href.push_str(doc_def.name.as_str());
let mut anchor_buf = String::new();
push_html(
&mut anchor_buf,
"a",
vec![("href", entry_href.as_str()), ("class", "type-ahead-link")],
entry_contents_buf.as_str(),
);
push_html(
&mut buf,
"li",
vec![("role", "option")],
anchor_buf.as_str(),
);
}
}
}
}
buf
}
pub fn load_module_for_docs(filename: PathBuf) -> LoadedModule {
let arena = Bump::new();
let load_config = LoadConfig {

View file

@ -19,9 +19,6 @@
<body>
<nav id="sidebar-nav">
<input id="module-search" aria-labelledby="search-link" type="text" placeholder="Search" />
<label for="module-search" id="search-link"><span id="search-link-text">Search</span> <span
id="search-link-hint">(press <span id="search-shortcut-key">s</span>)</span></label>
<div class="module-links">
<!-- Module links -->
</div>
@ -41,6 +38,23 @@
</a>
<!-- Package Name -->
</div>
<form id="module-search-form">
<input
id="module-search"
aria-labelledby="search-link"
type="text"
placeholder="Search"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="search-type-ahead"
/>
<label for="module-search" id="search-link">Search (press s)</label>
<span id="search-shortcut-key" aria-hidden="true">s</span>
<ul id="search-type-ahead" role="listbox" aria-label="Search Results" class="hidden">
<!-- Search Type Ahead -->
</ul>
</form>
<div class="top-header-triangle">
<!-- if the window gets big, this extends the purple bar on the top header to the left edge of the window -->
</div>

View file

@ -1,69 +1,88 @@
(() => {
let sidebar = document.getElementById("sidebar-nav");
// Un-hide everything
sidebar
.querySelectorAll(".sidebar-entry a")
.forEach((entry) => entry.classList.remove("hidden"));
// Re-hide all the sub-entries except for those of the current module
let currentModuleName = document.querySelector(".module-name").textContent;
sidebar.querySelectorAll(".sidebar-entry").forEach((entry) => {
let entryName = entry.querySelector(".sidebar-module-link").textContent;
if (currentModuleName === entryName) {
entry.firstChild.classList.add("active");
return;
}
entry
.querySelectorAll(".sidebar-sub-entries a")
.forEach((subEntry) => subEntry.classList.add("hidden"));
});
let searchTypeAhead = document.getElementById("search-type-ahead");
let searchBox = document.getElementById("module-search");
let searchForm = document.getElementById("module-search-form");
let topSearchResultListItem = undefined;
if (searchBox != null) {
function search() {
topSearchResultListItem = undefined;
let text = searchBox.value.toLowerCase(); // Search is case-insensitive.
if (text === "") {
// Un-hide everything
sidebar
.querySelectorAll(".sidebar-entry a")
.forEach((entry) => entry.classList.remove("hidden"));
if (text === "") {
searchTypeAhead.classList.add("hidden");
} else {
let totalResults = 0;
// Firsttype-ahead-signature", show/hide all the sub-entries within each module (top-level functions etc.)
searchTypeAhead.querySelectorAll("li").forEach((entry) => {
const entryName = entry
.querySelector(".type-ahead-def-name")
.textContent.toLowerCase();
const entrySignature = entry
.querySelector(".type-ahead-signature")
.textContent.toLowerCase()
.replace(/\s+/g, "");
// Re-hide all the sub-entries except for those of the current module
let currentModuleName =
document.querySelector(".module-name").textContent;
sidebar.querySelectorAll(".sidebar-entry").forEach((entry) => {
let entryName = entry.querySelector(
".sidebar-module-link"
).textContent;
if (currentModuleName === entryName) {
entry.firstChild.classList.add("active");
return;
}
entry
.querySelectorAll(".sidebar-sub-entries a")
.forEach((subEntry) =>
subEntry.classList.add("hidden")
);
});
} else {
// First, show/hide all the sub-entries within each module (top-level functions etc.)
sidebar
.querySelectorAll(".sidebar-sub-entries a")
.forEach((entry) => {
if (entry.textContent.toLowerCase().includes(text)) {
entry.classList.remove("hidden");
} else {
entry.classList.add("hidden");
}
});
// Then, show/hide modules based on whether they match, or any of their sub-entries matched
sidebar
.querySelectorAll(".sidebar-module-link")
.forEach((entry) => {
if (
entry.textContent.toLowerCase().includes(text) ||
entry.parentNode.querySelectorAll(
".sidebar-sub-entries a:not(.hidden)"
).length > 0
totalResults < 5 &&
(entryName.includes(text) ||
entrySignature.includes(text.replace(/\s+/g, "")))
) {
totalResults++;
entry.classList.remove("hidden");
if (topSearchResultListItem === undefined) {
topSearchResultListItem = entry;
}
} else {
entry.classList.add("hidden");
}
});
}
if (totalResults < 1) {
searchTypeAhead.classList.add("hidden");
} else {
searchTypeAhead.classList.remove("hidden");
}
}
}
searchBox.addEventListener("input", search);
search();
function searchSubmit(e) {
// pick the top result if the user submits search form
e.preventDefault();
if (topSearchResultListItem !== undefined) {
let topSearchResultListItemAnchor =
topSearchResultListItem.querySelector("a");
if (topSearchResultListItemAnchor !== null) {
topSearchResultListItemAnchor.click();
}
}
}
searchForm.addEventListener("submit", searchSubmit);
// Capture '/' keypress for quick search
window.addEventListener("keyup", (e) => {
if (e.key === "s" && document.activeElement !== searchBox) {
@ -77,13 +96,20 @@
// De-focus input box
searchBox.blur();
} else if (
e.key === "Escape" &&
searchTypeAhead.contains(document.activeElement)
) {
e.preventDefault;
// Reset sidebar state
search();
// De-focus type ahead
searchBox.focus();
searchBox.blur();
}
});
}
const isTouchSupported = () => {
try {
document.createEvent("TouchEvent");

View file

@ -24,6 +24,9 @@
monospace;
--top-header-height: 67px;
--sidebar-width: 280px;
--module-search-height: 48px;
--module-search-padding-height: 12px;
--module-search-form-padding-width: 20px;
}
a {
@ -439,40 +442,37 @@ pre>samp {
display: none !important;
}
#module-search:placeholder-shown {
padding: 0;
opacity: 0;
height: 0;
#module-search-form {
display: flex;
align-items: center;
align-content: center;
height: 100%;
background-color: var(--violet-bg);
position: relative;
max-width: 500px;
flex-grow: 1;
box-sizing: border-box;
padding: 0px var(--module-search-form-padding-width);
}
#module-search,
#module-search:focus {
opacity: 1;
padding: 12px 16px;
height: 48px;
}
/* Show the "Search" label link when the text input has a placeholder */
#module-search:placeholder-shown + #search-link {
display: flex;
}
/* Hide the "Search" label link when the text input has focus */
#module-search:focus + #search-link {
display: none;
height: var(--module-search-height);
}
#module-search {
display: block;
position: relative;
box-sizing: border-box;
width: 100%;
box-sizing: border-box;
font-size: 18px;
line-height: 18px;
margin-top: 6px;
border: none;
color: var(--faded-color);
background-color: var(--code-bg);
background-color: var(--body-bg-color);
font-family: var(--font-serif);
}
@ -481,6 +481,62 @@ pre>samp {
opacity: 1;
}
#module-search-form:focus-within #search-type-ahead {
display: flex;
gap: 20px;
flex-direction: column;
position: absolute;
top: calc(var(--module-search-padding-height) + var(--module-search-height));
left: var(--module-search-form-padding-width);
width: calc(100% - 2 * var(--module-search-form-padding-width));
}
#search-type-ahead {
box-sizing: border-box;
display: none;
z-index: 100;
background-color: var(--body-bg-color);
border-width: 1px;
border-style: solid;
border-color: var(--border-color);
list-style-type: none;
margin: 0;
padding: 10px;
}
#search-type-ahead .type-ahead-link {
font-size: 0.875rem;
color: var(--text-color);
p {
margin: 0px;
}
p.type-ahead-def-name {
color: var(--violet);
font-size: 1rem;
}
p.type-ahead-doc-path {
font-size: 0.75rem;
color: var(--faded-color);
}
}
#search-type-ahead li {
box-sizing: border-box;
padding: 4px;
}
/*use browser defaults for focus outline*/
#search-type-ahead li:focus-within {
/*firefox*/
outline: 5px auto Highlight;
/*chrome and safari*/
outline: 5px auto -webkit-focus-ring-color;
}
.type-ahead-link:focus-visible {
outline: none;
}
#search-link {
box-sizing: border-box;
display: none;
@ -492,21 +548,16 @@ pre>samp {
cursor: pointer;
}
#search-link:hover #search-link-text {
text-decoration: underline;
}
#search-link-hint {
margin-left: 1em;
opacity: 0.6;
}
#search-shortcut-key {
font-family: monospace;
border: 1px solid #666;
border-radius: 5px;
padding: 1px 3px 3px;
font-style: normal;
line-height: 15px;
opacity: 0.6;
position: absolute;
right: 30px;
}
.builtins-tip {
@ -546,7 +597,11 @@ pre>samp {
}
@media only screen and (max-device-width: 480px) and (orientation: portrait) {
#search-link-hint {
:root {
--top-header-height: 140px;
}
#search-shortcut-key {
display: none;
}
@ -555,8 +610,9 @@ pre>samp {
}
.top-header {
flex-direction: column;
height: auto;
justify-content: space-between;
width: auto;
/* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants. */
min-width: 0;
}