Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Submodule path 'themes/srcf-hugo-theme': checked out 'f6c43f8ca31241acb44c4a7493

### TODO

* add search
* refine search
* vendor static assets centrally

## Credits
Expand Down
3 changes: 3 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ enableGitInfo = true
description = "The Student-Run Computing Facility is a volunteer-run student society that provides free, useful and flexible computing and network services for Cambridge University staff and students of all degrees of ability."
repo = "https://github.com/srcf/docs"
repo_branch = "master"

[outputs]
home = ["HTML", "RSS", "JSON"]
10 changes: 9 additions & 1 deletion layouts/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{{ define "main" }}
<div class="container p-3 py-md-5">
<h1>SRCF Documentation</h1>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center">
<h1>SRCF Documentation</h1>

<form class="srcf-search">
<input type="search" class="form-control" id="srcf-search-input" placeholder="Search docs..." aria-label="Search docs for..." autocomplete="off"/>
<ul id="srcf-search-results" class="dropdown-menu d-block d-none"></ul>
</form>
</div>

<p class="fs-5 col-md-8">
Hello there! This is the Student-Run Computing Facility's documentation.
Our revamped docs contain all manner of useful pages, such as getting
Expand Down
8 changes: 8 additions & 0 deletions layouts/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{{- $index := slice -}}
{{- range .Site.RegularPages -}}
{{- $cleanContents := .Plain | htmlUnescape -}}
{{- $headingTags := findRE "<a.*? href=\"#.*?\".*?>.*?</a>" .TableOfContents -}}

{{- $index = $index | append (dict "title" .Title "contents" $cleanContents "headingTags" $headingTags "permalink" .Permalink) -}}
{{- end -}}
{{- $index | jsonify -}}
6 changes: 6 additions & 0 deletions layouts/partials/scripts.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/flexsearch@0.8.212/dist/flexsearch.bundle.min.js"
integrity="sha256-YuM9LRhJPr00HlaS324Mf8dKJL8Djydw+QP22hlSUAM="
crossorigin="anonymous"
></script>
<script src="/js/copy-button.js"></script>
<script src="/js/search.js"></script>
<!-- <script src="/js/main.js"></script> -->
<!-- <script src="{{ .Site.Params.domain_web }}/_srcf/vendor/js/popper.min.js"></script>
<script src="{{ .Site.Params.domain_web }}/_srcf/vendor/js/bootstrap.min.js"></script> -->
11 changes: 5 additions & 6 deletions layouts/partials/subnav.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<nav class="srcf-subnavbar py-2" aria-label="secondary navigation">
<div class="container-xxl d-flex align-items-md-center justify-content-end">
{{/*
<form class="srcf-search me-auto">
<input type="search" class="form-control" id="search-input" placeholder="Search docs..." aria-label="Search docs for..." autocomplete="off">
<form class="srcf-search ms-auto">
<input type="search" class="form-control" id="srcf-search-input" placeholder="Search docs..." aria-label="Search docs for..." autocomplete="off"/>
<ul id="srcf-search-results" class="dropdown-menu d-block d-none"></ul>
</form>
*/}}

{{ partial "subnav-categories" . }}

<button class="btn srcf-sidebar-toggle d-md-none py-0 px-1 ms-3 order-3 collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#srcf-docs-nav" aria-controls="srcf-docs-nav" aria-expanded="false" aria-label="Toggle docs navigation">
{{ partial "icons/expand.svg" (dict "class" "bi bi-expand" "width" "24" "height" "24") }}
{{ partial "icons/collapse.svg" (dict "class" "bi bi-collapse" "width" "24" "height" "24") }}
</button>
</div>
</nav>
</nav>
17 changes: 17 additions & 0 deletions static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,23 @@ docs-specific classes
box-shadow: 0 0 0 3px rgba(121, 82, 179, 0.25);
}

#srcf-search-results {
font-size: 1rem;
max-height: 80vh;
overflow: auto;
Comment thread
rsa33 marked this conversation as resolved.
Outdated
min-width: 244px; /* to match parent width */
}

#srcf-search-results a.active {
color: #1e2125;
background-color: #e9ecef;
}

#srcf-search-results .search-result-subheader {
display: block;
margin-top: -4px;
}

.srcf-sidebar-toggle {
color: #6c757d;
}
Expand Down
195 changes: 195 additions & 0 deletions static/js/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
window.addEventListener("DOMContentLoaded", function() {
const elSearchQuery = document.getElementById("srcf-search-input");
const elSearchResults = document.getElementById("srcf-search-results");

let selectedIndex = 0;

let searcher = null;
let searchIndex = null;
let searcherFetcher = null;

elSearchQuery.addEventListener("input", async function(event) {
if (searcherFetcher === null) {
searcherFetcher = fetch("/index.json")
.then(response => response.json())
.then(index => {
searchIndex = index;
fixupIndex(searchIndex);

searcher = new FlexSearch.Document({
document: {
id: "id",
index: [
{
field: "title-forward",
tokenize: "forward",
boost: 3
},
{
field: "title-tolerant",
tokenize: "tolerant",
boost: 3
},
{
field: "heading-title-forward",
tokenize: "forward",
boost: 1
},
{
field: "heading-title-tolerant",
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be able to remove these? can't seem to get a meaningful difference in results with/without them - I think suggest: true might cover the same case

tokenize: "tolerant",
boost: 1
},
{
field: "contents",
tokenize: "tolerant"
}
]
},
});

searchIndex.forEach(item => {
let headings = item.headings.map(h => h.title);
searcher.add({
"id": item.id,
"contents": item.contents,
"title-forward": item.title,
"title-tolerant": item.title,
"heading-title-tolerant": headings,
"heading-title-forward": headings,
"permalink": item.permalink
});
});
})
.catch(error => {
console.error("Error loading search index:", error);
searcherFetcher = null;
});
}

if (searcher === null) {
await searcherFetcher;
}

const searchQuery = event.target.value;

if (searchQuery.length > 0) {
elSearchResults.classList.remove("d-none");
executeSearch(searchQuery);
selectedIndex = 0;
updateSelected();
} else {
elSearchResults.classList.add("d-none");
}
});

elSearchQuery.addEventListener("keydown", function(event) {
const resultCount = elSearchResults.children.length;
if (event.key === "ArrowDown") {
event.preventDefault();
selectedIndex = (selectedIndex + 1) % resultCount;
updateSelected();
} else if (event.key === "ArrowUp") {
event.preventDefault();
selectedIndex = (selectedIndex - 1 + resultCount) % resultCount;
updateSelected();
} else if (event.key === "Enter") {
event.preventDefault();
elSearchResults.children[selectedIndex].click();
}
});

window.addEventListener("keydown", function(event) {
if (event.key === "/" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
elSearchQuery.focus();
elSearchQuery.select();
}
});

window.addEventListener("mousedown", function(event) {
if (!elSearchResults.contains(event.target) && event.target !== elSearchQuery) {
elSearchResults.classList.add("d-none");
}
});

function updateSelected() {
for (let child of elSearchResults.children) {
child.classList.remove("active");
if (child === elSearchResults.children[selectedIndex]) {
child.classList.add("active");
}
}
}

function fixupIndex(index) {
const parser = new DOMParser();

index.forEach((item, index) => {
item.id = index;

if (item.headingTags) {
item.headings = item.headingTags.map(tag => {
const element = parser.parseFromString(tag, "text/html").body.firstChild
return {
id: element.getAttribute("href").slice(1),
title: element.textContent
};
});
} else {
item.headings = [];
}
});
}

function executeSearch(searchQuery) {
var results = searcher.search({ query: searchQuery, merge: true, suggest: true });
console.log(results);

elSearchResults.innerHTML = "";

if (results.length == 0) {
const elNoResults = document.createElement("div");
elNoResults.className = "dropdown-item disabled";
elNoResults.innerText = "No matches found";
elSearchResults.appendChild(elNoResults);
return;
}

results.forEach((result, index) => {
const doc = searchIndex.find(doc => doc.id == result.id);

const elResult = document.createElement("a");
elResult.className = "dropdown-item";

elResult.href = doc.permalink;
elResult.innerText = doc.title;

// if the best match was a heading, conservatively estimate which one it was
// unfortunately this is the best we have as flexsearch won't highlight arrays at the moment
// TODO: find some kind of fix for the above
if (result.field.some(field => field.startsWith("heading-title")) && !result.field.some(field => field.startsWith("title"))) {
let terms = searchQuery.trim().toLowerCase().split(/ +/);
let estimatedHeadings = doc.headings.filter(h => terms.some(term => h.title.toLowerCase().includes(term)));

// if we know exactly which heading it was, show it.
// do not show if there are multiple matches in case we picked the wrong one - this could mislead
// users into thinking the content they want is not in the page as it is not under the suggested heading.
if (estimatedHeadings.length == 1) {
const subheader = document.createElement("small");
subheader.className = "text-muted search-result-subheader";
subheader.innerText = estimatedHeadings[0].title;
elResult.href += "#" + estimatedHeadings[0].id;
elResult.appendChild(subheader);
}
}

elResult.addEventListener("mousemove", function() {
selectedIndex = index;
updateSelected();
});

elSearchResults.appendChild(elResult);
});
}
});