Skip to content

Commit

Permalink
feat: add search
Browse files Browse the repository at this point in the history
  • Loading branch information
charlesrocket committed Mar 18, 2024
1 parent 10500ed commit 0ade950
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 3 deletions.
9 changes: 8 additions & 1 deletion config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ author = "-k"
compile_sass = true
minify_html = false
generate_feed = true
build_search_index = false
build_search_index = true

taxonomies = [
{ name = "categories", feed = true },
{ name = "tags", feed = true },
]

[search]
include_title = true
include_description = true
include_path = false
include_content = true
index_format = "elasticlunr_json"

[markdown]
highlight_code = true
highlight_theme = "css"
Expand Down
85 changes: 85 additions & 0 deletions sass/_search.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.search-container {
position: absolute;
font-size: 12px;
top: 20px;
left: 20px;
width: 320px;
z-index: 2;
input {
border: 1px solid darken($white, 42);
padding: 0.5rem;
width: 100%;
}
}

#search-nav {
display: none;
}

.search-results {
display: none;
top: 40px;
position: absolute;
background-color: $white;
padding: 1rem;
max-height: 500px;
width: 150%;
overflow-y: auto;
border: 2px solid;
box-shadow: 3px 3px 3px 2px rgba(0, 0, 0, 0.1);
&__items {
list-style: none;
padding: 1rem;
z-index: 3;
}
li {
margin-top: 1rem;
border-bottom: 1px solid #ccc;
&:first-of-type {
margin-top: 0;
}
}
&__item {
margin-bottom: 1rem;
a {
font-size: 1.2rem;
display: inline-block;
margin-bottom: 0.5rem;
-webkit-text-decoration-line: underline;
text-decoration-line: underline;
-webkit-text-decoration-style: dashed;
text-decoration-style: dashed;
text-decoration-color: $green;
text-decoration-thickness: 2px;
}
}
}

@media screen and (max-width: 640px) {
.search-container {
top: 70px;
left: 50%;
right: unset;
margin-right: -50%;
transform: translate(-50%, 0);
}
.search-results {
left: 50%;
right: unset;
margin-right: -50%;
transform: translate(-50%, 0);
}
}

@media (prefers-color-scheme: dark) {
.search-container {
input {
color: $white;
border-color: darken($white, 30);
background-color: $dark-blue;
}
}
.search-results {
background-color: $dark-blue;
}
}
2 changes: 1 addition & 1 deletion sass/_site.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ nav.nav-bar {
position: absolute;
top: 20px;
right: 20px;
z-index: 1;
z-index: 2;
}

nav.nav-bar ul {
Expand Down
1 change: 1 addition & 0 deletions sass/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@import "reset";
@import "base";
@import "site";
@import "search";
@import "utilities";
@import "syntax";
@import "vendor/fonts";
Expand Down
10 changes: 10 additions & 0 deletions static/elasticlunr.min.js

Large diffs are not rendered by default.

194 changes: 194 additions & 0 deletions static/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
document.getElementById('search-nav').style.display='block';

function debounce(func, wait) {
var timeout;

return function () {
var context = this;
var args = arguments;

clearTimeout(timeout);
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
};
}

function makeTeaser(body, terms) {
var TERM_WEIGHT = 40;
var NORMAL_WORD_WEIGHT = 2;
var FIRST_WORD_WEIGHT = 8;
var TEASER_MAX_WORDS = 30;

var stemmedTerms = terms.map(function (w) {
return elasticlunr.stemmer(w.toLowerCase());
});

var termFound = false;
var index = 0;
var weighted = [];
var sentences = body.toLowerCase().split(". ");

for (var i in sentences) {
var words = sentences[i].split(" ");
var value = FIRST_WORD_WEIGHT;

for (var j in words) {
var word = words[j];

if (word.length > 0) {
for (var k in stemmedTerms) {
if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
value = TERM_WEIGHT;
termFound = true;
}
}
weighted.push([word, value, index]);
value = NORMAL_WORD_WEIGHT;
}

index += word.length;
index += 1;
}

index += 1;
}

if (weighted.length === 0) {
return body;
}

var windowWeights = [];
var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
var curSum = 0;

for (var i = 0; i < windowSize; i++) {
curSum += weighted[i][1];
}

windowWeights.push(curSum);

for (var i = 0; i < weighted.length - windowSize; i++) {
curSum -= weighted[i][1];
curSum += weighted[i + windowSize][1];
windowWeights.push(curSum);
}

var maxSumIndex = 0;

if (termFound) {
var maxFound = 0;
for (var i = windowWeights.length - 1; i >= 0; i--) {
if (windowWeights[i] > maxFound) {
maxFound = windowWeights[i];
maxSumIndex = i;
}
}
}

var teaser = [];
var startIndex = weighted[maxSumIndex][2];

for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
var word = weighted[i];
if (startIndex < word[2]) {
teaser.push(body.substring(startIndex, word[2]));
startIndex = word[2];
}

if (word[1] === TERM_WEIGHT) {
teaser.push("<b>");
}
startIndex = word[2] + word[0].length;
teaser.push(body.substring(word[2], startIndex));

if (word[1] === TERM_WEIGHT) {
teaser.push("</b>");
}
}

teaser.push(" …");
return teaser.join("");
}

function formatSearchResultItem(item, terms) {
return '<div class="search-results__item">'
+ `<a href="${item.ref}">${item.doc.title}</a>`
+ `<div>${makeTeaser(item.doc.body, terms)}</div>`
+ '</div>';
}

function initSearch() {
var $searchInput = document.getElementById("search");
var $searchResults = document.querySelector(".search-results");
var $searchResultsItems = document.querySelector(".search-results__items");
var MAX_ITEMS = 10;

var options = {
bool: "AND",
fields: {
title: {boost: 2},
body: {boost: 1},
}
};

var currentTerm = "";
var index;

var initIndex = async function () {
if (index === undefined) {
index = fetch("/search_index.en.json")
.then(
async function(response) {
return await elasticlunr.Index.load(await response.json());
}
);
}

let res = await index;
return res;
}

$searchInput.addEventListener("keyup", debounce(async function() {
var term = $searchInput.value.trim();
if (term === currentTerm) {
return;
}

$searchResults.style.display = term === "" ? "none" : "block";
$searchResultsItems.innerHTML = "";
currentTerm = term;

if (term === "") {
return;
}

var results = (await initIndex()).search(term, options);
if (results.length === 0) {
$searchResults.style.display = "none";
return;
}

for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
var item = document.createElement("li");
item.innerHTML = formatSearchResultItem(results[i], term.split(" "));
$searchResultsItems.appendChild(item);
}
}, 150));

window.addEventListener('click', function(e) {
if ($searchResults.style.display == "block" && !$searchResults.contains(e.target)) {
$searchResults.style.display = "none";
}
});
}


if (document.readyState === "complete" ||
(document.readyState !== "loading" && !document.documentElement.doScroll)
) {
initSearch();
} else {
document.addEventListener("DOMContentLoaded", initSearch);
}
10 changes: 10 additions & 0 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ <h1 class="section-title">{%- block page_title %}{% if page.title %}{{ page.titl
</div>
</div>
<div class="block-right">
{% if config.build_search_index == true %}
<script type="text/javascript" src="{{ get_url(path="/elasticlunr.min.js", cachebust=true) }}" async></script>
<script type="text/javascript" src="{{ get_url(path="/search.js", cachebust=true) }}" defer></script>
<nav class="search-container" id="search-nav">
<input id="search" type="search" placeholder="Search" aria-label="Search">
<div class="search-results">
<div class="search-results__items"></div>
</div>
</nav>
{% endif %}
<nav class="nav-bar" role="navigation">
<ul class="nav-menu">
{% block nav %}
Expand Down
2 changes: 1 addition & 1 deletion templates/partials/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<meta name="base" content="{{ config.base_url | safe }}"/>
<meta name="referrer" content="strict-origin-when-cross-origin"/>
{% if config.extra.csp == true %}{% block csp %}
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src 'self'; img-src 'self' {% if page.extra.image %}{% if page.extra.image is matching("^http[s]?://") %}{{ page.extra.image }}{% endif %}{% endif %} {% if config.extra.images.home is matching("^http[s]?://") %}{{ config.extra.images.home }}{% endif %} {% if config.extra.images.post_list is matching("^http[s]?://") %}{{ config.extra.images.post_list }}{% endif %} {% if category_image_match %}{% if category_image_match is matching("^http[s]?://") %} {{ category_image }}{% endif %}{% endif %}{% if config.extra.images.default_post is matching("^http[s]?://") %} {{ config.extra.images.default_post }}{% endif %}{% if page.extra.csp_img %}{%for url in page.extra.csp_img %} {{ url }}{% endfor %}{% endif %}{% if section.extra.csp_img %}{%for url in section.extra.csp_img %} {{ url }}{% endfor %}{% endif %}; script-src 'self' {% if config.extra.comments.system == "giscus" %}giscus.app/client.js{% endif %}{% if config.extra.comments.system == "cactus" %} 'sha512-{{ cactus_hash | safe }}'{% endif %}; manifest-src 'self'; style-src 'self' {% if page_image_hash %}'sha512-{{ page_image_hash | safe }}'{% elif category_image %}'sha512-{{ category_image_hash |safe }}'{% else %}'sha512-{{ default_post_image_hash | safe }}'{% endif %} 'sha512-{{ main_images_hash | safe }}' {% if config.extra.comments.system == "giscus" %}giscus.app/default.css{% endif %}; media-src 'self'; frame-src 'self' https://player.vimeo.com/ https://www.youtube.com/ https://www.youtube-nocookie.com/ {% if config.extra.comments.system == "giscus" %}giscus.app{% endif %}; object-src 'none'; base-uri 'self'; form-action 'self'; connect-src {% if config.extra.comments.system == "cactus" %}https://matrix.cactus.chat/ {% endif %}{% if config.mode == "serve" %} ws://127.0.0.1:1024/livereload{% endif %}{% if config.mode != "serve" and config.extra.comments.system != "cactus" and config.extra.comments.system != "giscus" %}'none'{% endif %}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src 'self'; img-src 'self' {% if page.extra.image %}{% if page.extra.image is matching("^http[s]?://") %}{{ page.extra.image }}{% endif %}{% endif %} {% if config.extra.images.home is matching("^http[s]?://") %}{{ config.extra.images.home }}{% endif %} {% if config.extra.images.post_list is matching("^http[s]?://") %}{{ config.extra.images.post_list }}{% endif %} {% if category_image_match %}{% if category_image_match is matching("^http[s]?://") %} {{ category_image }}{% endif %}{% endif %}{% if config.extra.images.default_post is matching("^http[s]?://") %} {{ config.extra.images.default_post }}{% endif %}{% if page.extra.csp_img %}{%for url in page.extra.csp_img %} {{ url }}{% endfor %}{% endif %}{% if section.extra.csp_img %}{%for url in section.extra.csp_img %} {{ url }}{% endfor %}{% endif %}; script-src 'self' {% if config.extra.comments.system == "giscus" %}giscus.app/client.js{% endif %}{% if config.extra.comments.system == "cactus" %} 'sha512-{{ cactus_hash | safe }}'{% endif %}; manifest-src 'self'; style-src 'self' {% if page_image_hash %}'sha512-{{ page_image_hash | safe }}'{% elif category_image %}'sha512-{{ category_image_hash |safe }}'{% else %}'sha512-{{ default_post_image_hash | safe }}'{% endif %} 'sha512-{{ main_images_hash | safe }}' {% if config.extra.comments.system == "giscus" %}giscus.app/default.css{% endif %}; media-src 'self'; frame-src 'self' https://player.vimeo.com/ https://www.youtube.com/ https://www.youtube-nocookie.com/ {% if config.extra.comments.system == "giscus" %}giscus.app{% endif %}; object-src 'none'; base-uri 'self'; form-action 'self'; connect-src {% if config.build_search_index == true %}http://127.0.0.1:1111/ {% endif %}{% if config.extra.comments.system == "cactus" %}https://matrix.cactus.chat/ {% endif %}{% if config.mode == "serve" %} ws://127.0.0.1:1024/livereload{% endif %}{% if config.mode != "serve" and config.extra.comments.system != "cactus" and config.extra.comments.system != "giscus" %}'none'{% endif %}">
{% endblock csp %}{% endif %}
<meta name="robots" content="index,follow">
<meta name="theme-color" content="#2C2D32"/>
Expand Down

0 comments on commit 0ade950

Please sign in to comment.