<button type="reset">Annuler</button> | <button type="reset">Annuler</button> | ||||
</form> | </form> | ||||
<div id="search-status" role="status"></div> | |||||
<p id="search-status" role="status"></p> | |||||
<div id="search-results"></div> | <div id="search-results"></div> | ||||
<hr> | <hr> | ||||
<p> | <p> | ||||
Seuls les écrits de ces dernières années sont indexés. | |||||
Vous pouvez aussi consulter les archives chronologiques de | Vous pouvez aussi consulter les archives chronologiques de | ||||
<a href="/david/2022/">2022</a>, | <a href="/david/2022/">2022</a>, | ||||
<a href="/david/2021/">2021</a>, | <a href="/david/2021/">2021</a>, | ||||
}) | }) | ||||
</script> | </script> | ||||
<template id="search-result"> | |||||
<h2> | |||||
<a href="${url}">${title}</a> (${date}) | |||||
</h2> | |||||
<p>${content}</p> | |||||
</template> | |||||
<script id="search-index" type="application/json"> | <script id="search-index" type="application/json"> | ||||
[ | [ | ||||
{ | { | ||||
let input = document.querySelector('#input-search') | let input = document.querySelector('#input-search') | ||||
let resultList = document.querySelector('#search-results') | let resultList = document.querySelector('#search-results') | ||||
let searchStatus = document.querySelector('#search-status') | let searchStatus = document.querySelector('#search-status') | ||||
let searchResultTemplate = document.querySelector('#search-result') | |||||
// Make sure required content exists | // 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 | // Create a submit handler | ||||
form.addEventListener('submit', function (event) { | form.addEventListener('submit', function (event) { | ||||
/** | /** | ||||
* Show the search results in the UI | * Show the search results in the UI | ||||
* @param {Array} results The results to display | * @param {Array} results The results to display | ||||
* @param {List} regMap Regular expressions for the highlights | |||||
*/ | */ | ||||
function showResults (results, regMap) { | function showResults (results, regMap) { | ||||
let status = 'Aucune publication n’a été trouvée 😢' | |||||
let searchResults = '' | |||||
if (results.length) { | if (results.length) { | ||||
const plural = results.length > 1 ? 's' : '' | 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('') | }).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, | // TODO: deal with close matches when multiple words are looked for, | ||||
// it does not look trivial because you have to memorize positions | // it does not look trivial because you have to memorize positions | ||||
// then create extracts. | // then create extracts. | ||||
// For instance: `microsoft github` | // For instance: `microsoft github` | ||||
const extractBoundariesSize = 100 | const extractBoundariesSize = 100 | ||||
const contentLength = content.length | |||||
const textLength = text.length | |||||
let extracts = [] | let extracts = [] | ||||
for (let reg of regMap) { | for (let reg of regMap) { | ||||
const index = content.search(reg) | |||||
const index = text.search(reg) | |||||
if (index === -1) { continue } | if (index === -1) { continue } | ||||
let extract = content.substring( | |||||
let extract = text.substring( | |||||
index - extractBoundariesSize, | index - extractBoundariesSize, | ||||
index + reg.source.length + extractBoundariesSize | index + reg.source.length + extractBoundariesSize | ||||
) | ) | ||||
// (is that confusing or closer to what is expected?) | // (is that confusing or closer to what is expected?) | ||||
extract = extract.replace(reg,`<mark>${reg.source}</mark>`) | extract = extract.replace(reg,`<mark>${reg.source}</mark>`) | ||||
const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | ||||
const suffixEllipsis = index - extractBoundariesSize <= contentLength ? '…' : '' | |||||
const suffixEllipsis = index + extractBoundariesSize <= textLength ? '…' : '' | |||||
extracts.push(`${prefixEllipsis}${extract}${suffixEllipsis}`) | 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('') | return extracts.join('') | ||||
} | } | ||||
<button type="reset">Annuler</button> | <button type="reset">Annuler</button> | ||||
</form> | </form> | ||||
<div id="search-status" role="status"></div> | |||||
<p id="search-status" role="status"></p> | |||||
<div id="search-results"></div> | <div id="search-results"></div> | ||||
<hr> | <hr> | ||||
<p> | <p> | ||||
Seuls les écrits de ces dernières années sont indexés. | |||||
Vous pouvez aussi consulter les archives chronologiques de | Vous pouvez aussi consulter les archives chronologiques de | ||||
<a href="/david/2022/">2022</a>, | <a href="/david/2022/">2022</a>, | ||||
<a href="/david/2021/">2021</a>, | <a href="/david/2021/">2021</a>, | ||||
{% endblock content %} | {% endblock content %} | ||||
{% block extra_body %} | {% 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"> | <script id="search-index" type="application/json"> | ||||
{{ search_index }} | {{ search_index }} | ||||
</script> | </script> | ||||
let input = document.querySelector('#input-search') | let input = document.querySelector('#input-search') | ||||
let resultList = document.querySelector('#search-results') | let resultList = document.querySelector('#search-results') | ||||
let searchStatus = document.querySelector('#search-status') | let searchStatus = document.querySelector('#search-status') | ||||
let searchResultTemplate = document.querySelector('#search-result') | |||||
// Make sure required content exists | // 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 | // Create a submit handler | ||||
form.addEventListener('submit', function (event) { | form.addEventListener('submit', function (event) { | ||||
/** | /** | ||||
* Show the search results in the UI | * Show the search results in the UI | ||||
* @param {Array} results The results to display | * @param {Array} results The results to display | ||||
* @param {List} regMap Regular expressions for the highlights | |||||
*/ | */ | ||||
function showResults (results, regMap) { | function showResults (results, regMap) { | ||||
let status = 'Aucune publication n’a été trouvée 😢' | |||||
let searchResults = '' | |||||
if (results.length) { | if (results.length) { | ||||
const plural = results.length > 1 ? 's' : '' | 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('') | }).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, | // TODO: deal with close matches when multiple words are looked for, | ||||
// it does not look trivial because you have to memorize positions | // it does not look trivial because you have to memorize positions | ||||
// then create extracts. | // then create extracts. | ||||
// For instance: `microsoft github` | // For instance: `microsoft github` | ||||
const extractBoundariesSize = 100 | const extractBoundariesSize = 100 | ||||
const contentLength = content.length | |||||
const textLength = text.length | |||||
let extracts = [] | let extracts = [] | ||||
for (let reg of regMap) { | for (let reg of regMap) { | ||||
const index = content.search(reg) | |||||
const index = text.search(reg) | |||||
if (index === -1) { continue } | if (index === -1) { continue } | ||||
let extract = content.substring( | |||||
let extract = text.substring( | |||||
index - extractBoundariesSize, | index - extractBoundariesSize, | ||||
index + reg.source.length + extractBoundariesSize | index + reg.source.length + extractBoundariesSize | ||||
) | ) | ||||
// (is that confusing or closer to what is expected?) | // (is that confusing or closer to what is expected?) | ||||
extract = extract.replace(reg,`<mark>${reg.source}</mark>`) | extract = extract.replace(reg,`<mark>${reg.source}</mark>`) | ||||
const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | const prefixEllipsis = index - extractBoundariesSize >= 0 ? '…' : '' | ||||
const suffixEllipsis = index - extractBoundariesSize <= contentLength ? '…' : '' | |||||
const suffixEllipsis = index + extractBoundariesSize <= textLength ? '…' : '' | |||||
extracts.push(`${prefixEllipsis}${extract}${suffixEllipsis}`) | 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('') | return extracts.join('') | ||||
} | } | ||||