@@ -80,12 +80,13 @@ | |||
<button type="reset">Annuler</button> | |||
</form> | |||
<div id="search-status" role="status"></div> | |||
<p id="search-status" role="status"></p> | |||
<div id="search-results"></div> | |||
<hr> | |||
<p> | |||
Seuls les écrits de ces dernières années sont indexés. | |||
Vous pouvez aussi consulter les archives chronologiques de | |||
<a href="/david/2022/">2022</a>, | |||
<a href="/david/2021/">2021</a>, | |||
@@ -221,6 +222,12 @@ | |||
}) | |||
</script> | |||
<template id="search-result"> | |||
<h2> | |||
<a href="${url}">${title}</a> (${date}) | |||
</h2> | |||
<p>${content}</p> | |||
</template> | |||
<script id="search-index" type="application/json"> | |||
[ | |||
{ | |||
@@ -5266,9 +5273,10 @@ | |||
let input = document.querySelector('#input-search') | |||
let resultList = document.querySelector('#search-results') | |||
let searchStatus = document.querySelector('#search-status') | |||
let searchResultTemplate = document.querySelector('#search-result') | |||
// Make sure required content exists | |||
if (!form || !input || !resultList || !searchStatus || !searchIndex || !stopWords) return | |||
if (!form || !input || !resultList || !searchStatus || !searchIndex || !stopWords || !searchResultTemplate) return | |||
// Create a submit handler | |||
form.addEventListener('submit', function (event) { | |||
@@ -5351,38 +5359,58 @@ | |||
/** | |||
* Show the search results in the UI | |||
* @param {Array} results The results to display | |||
* @param {List} regMap Regular expressions for the highlights | |||
*/ | |||
function showResults (results, regMap) { | |||
let status = 'Aucune publication n’a été trouvée 😢' | |||
let searchResults = '' | |||
if (results.length) { | |||
const plural = results.length > 1 ? 's' : '' | |||
searchStatus.innerHTML = `<p>${results.length} publication${plural} trouvée${plural} 🙌</p>` | |||
resultList.innerHTML = results.map(function (result) { | |||
return ` | |||
<h2> | |||
<a href="${result.article.url}">${result.article.title}</a> | |||
(${result.article.date}) | |||
</h2> | |||
<p>${highlightContent(result.article.content, regMap)}</p> | |||
` | |||
status = `${results.length} publication${plural} trouvée${plural} 🙌` | |||
searchResults = results.map(function (result) { | |||
return interpolate(searchResultTemplate.innerHTML, { | |||
url: result.article.url, | |||
title: highlightText(result.article.title, regMap), | |||
date: result.article.date, | |||
content: highlightText(result.article.content, regMap), | |||
}) | |||
}).join('') | |||
} else { | |||
searchStatus.innerHTML = '<p>Aucune publication n’a été trouvée 😢<br>Seuls les écrits de ces dernières années sont indexés.</p>' | |||
resultList.innerHTML = '' | |||
} | |||
searchStatus.innerHTML = status | |||
resultList.innerHTML = searchResults | |||
} | |||
/** | |||
* Get a template from a string | |||
* https://stackoverflow.com/a/41015840 | |||
* https://gomakethings.com/html-templates-with-vanilla-javascript/ | |||
* @param {String} str The string to interpolate | |||
* @param {Object} params The parameters | |||
* @return {String} The interpolated string | |||
*/ | |||
function interpolate (str, params) { | |||
let names = Object.keys(params); | |||
let vals = Object.values(params); | |||
return new Function(...names, `return \`${str}\``)(...vals); | |||
} | |||
function highlightContent(content, regMap) { | |||
/** | |||
* Highlight the text in the UI | |||
* @param {String} text The content to highlight | |||
* @param {List} regMap Regular expressions for the highlights | |||
*/ | |||
function highlightText(text, regMap) { | |||
// TODO: deal with close matches when multiple words are looked for, | |||
// it does not look trivial because you have to memorize positions | |||
// then create extracts. | |||
// For instance: `microsoft github` | |||
const extractBoundariesSize = 100 | |||
const contentLength = content.length | |||
const textLength = text.length | |||
let extracts = [] | |||
for (let reg of regMap) { | |||
const index = content.search(reg) | |||
const index = text.search(reg) | |||
if (index === -1) { continue } | |||
let extract = content.substring( | |||
let extract = text.substring( | |||
index - extractBoundariesSize, | |||
index + reg.source.length + extractBoundariesSize | |||
) | |||
@@ -5391,9 +5419,13 @@ | |||
// (is that confusing or closer to what is expected?) | |||
extract = extract.replace(reg,`<mark>${reg.source}</mark>`) | |||
const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | |||
const suffixEllipsis = index - extractBoundariesSize <= contentLength ? '…' : '' | |||
const suffixEllipsis = index + extractBoundariesSize <= textLength ? '…' : '' | |||
extracts.push(`${prefixEllipsis}${extract}${suffixEllipsis}`) | |||
} | |||
if (!extracts.length && textLength < 200) { | |||
// If there is no match but it's a short title, return it. | |||
return text | |||
} | |||
return extracts.join('') | |||
} | |||
@@ -30,12 +30,13 @@ | |||
<button type="reset">Annuler</button> | |||
</form> | |||
<div id="search-status" role="status"></div> | |||
<p id="search-status" role="status"></p> | |||
<div id="search-results"></div> | |||
<hr> | |||
<p> | |||
Seuls les écrits de ces dernières années sont indexés. | |||
Vous pouvez aussi consulter les archives chronologiques de | |||
<a href="/david/2022/">2022</a>, | |||
<a href="/david/2021/">2021</a>, | |||
@@ -45,6 +46,12 @@ | |||
{% endblock content %} | |||
{% block extra_body %} | |||
<template id="search-result"> | |||
<h2> | |||
<a href="${url}">${title}</a> (${date}) | |||
</h2> | |||
<p>${content}</p> | |||
</template> | |||
<script id="search-index" type="application/json"> | |||
{{ search_index }} | |||
</script> | |||
@@ -163,9 +170,10 @@ | |||
let input = document.querySelector('#input-search') | |||
let resultList = document.querySelector('#search-results') | |||
let searchStatus = document.querySelector('#search-status') | |||
let searchResultTemplate = document.querySelector('#search-result') | |||
// Make sure required content exists | |||
if (!form || !input || !resultList || !searchStatus || !searchIndex || !stopWords) return | |||
if (!form || !input || !resultList || !searchStatus || !searchIndex || !stopWords || !searchResultTemplate) return | |||
// Create a submit handler | |||
form.addEventListener('submit', function (event) { | |||
@@ -248,38 +256,58 @@ | |||
/** | |||
* Show the search results in the UI | |||
* @param {Array} results The results to display | |||
* @param {List} regMap Regular expressions for the highlights | |||
*/ | |||
function showResults (results, regMap) { | |||
let status = 'Aucune publication n’a été trouvée 😢' | |||
let searchResults = '' | |||
if (results.length) { | |||
const plural = results.length > 1 ? 's' : '' | |||
searchStatus.innerHTML = `<p>${results.length} publication${plural} trouvée${plural} 🙌</p>` | |||
resultList.innerHTML = results.map(function (result) { | |||
return ` | |||
<h2> | |||
<a href="${result.article.url}">${result.article.title}</a> | |||
(${result.article.date}) | |||
</h2> | |||
<p>${highlightContent(result.article.content, regMap)}</p> | |||
` | |||
status = `${results.length} publication${plural} trouvée${plural} 🙌` | |||
searchResults = results.map(function (result) { | |||
return interpolate(searchResultTemplate.innerHTML, { | |||
url: result.article.url, | |||
title: highlightText(result.article.title, regMap), | |||
date: result.article.date, | |||
content: highlightText(result.article.content, regMap), | |||
}) | |||
}).join('') | |||
} else { | |||
searchStatus.innerHTML = '<p>Aucune publication n’a été trouvée 😢<br>Seuls les écrits de ces dernières années sont indexés.</p>' | |||
resultList.innerHTML = '' | |||
} | |||
searchStatus.innerHTML = status | |||
resultList.innerHTML = searchResults | |||
} | |||
/** | |||
* Get a template from a string | |||
* https://stackoverflow.com/a/41015840 | |||
* https://gomakethings.com/html-templates-with-vanilla-javascript/ | |||
* @param {String} str The string to interpolate | |||
* @param {Object} params The parameters | |||
* @return {String} The interpolated string | |||
*/ | |||
function interpolate (str, params) { | |||
let names = Object.keys(params); | |||
let vals = Object.values(params); | |||
return new Function(...names, `return \`${str}\``)(...vals); | |||
} | |||
function highlightContent(content, regMap) { | |||
/** | |||
* Highlight the text in the UI | |||
* @param {String} text The content to highlight | |||
* @param {List} regMap Regular expressions for the highlights | |||
*/ | |||
function highlightText(text, regMap) { | |||
// TODO: deal with close matches when multiple words are looked for, | |||
// it does not look trivial because you have to memorize positions | |||
// then create extracts. | |||
// For instance: `microsoft github` | |||
const extractBoundariesSize = 100 | |||
const contentLength = content.length | |||
const textLength = text.length | |||
let extracts = [] | |||
for (let reg of regMap) { | |||
const index = content.search(reg) | |||
const index = text.search(reg) | |||
if (index === -1) { continue } | |||
let extract = content.substring( | |||
let extract = text.substring( | |||
index - extractBoundariesSize, | |||
index + reg.source.length + extractBoundariesSize | |||
) | |||
@@ -288,9 +316,13 @@ | |||
// (is that confusing or closer to what is expected?) | |||
extract = extract.replace(reg,`<mark>${reg.source}</mark>`) | |||
const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | |||
const suffixEllipsis = index - extractBoundariesSize <= contentLength ? '…' : '' | |||
const suffixEllipsis = index + extractBoundariesSize <= textLength ? '…' : '' | |||
extracts.push(`${prefixEllipsis}${extract}${suffixEllipsis}`) | |||
} | |||
if (!extracts.length && textLength < 200) { | |||
// If there is no match but it's a short title, return it. | |||
return text | |||
} | |||
return extracts.join('') | |||
} | |||