From 117f1ee6ee3a65b25d45f4f1875aec48041d5055 Mon Sep 17 00:00:00 2001 From: Alvie Rahman Date: Wed, 22 Dec 2021 21:16:09 +0000 Subject: [PATCH] Add search bar to table of contents --- Makefile | 1 + notes2web.py | 2 + styles.css | 9 +++- templates/article.html | 3 ++ toc_search.js | 94 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 toc_search.js diff --git a/Makefile b/Makefile index f42fe9d..9ca97b7 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ install: cp styles.css /opt/notes2web cp fuse.js /opt/notes2web cp search.js /opt/notes2web + cp toc_search.js /opt/notes2web uninstall: rm -rf /usr/local/bin/notes2web.py /opt/notes2web diff --git a/notes2web.py b/notes2web.py index b7728c1..f11ce77 100755 --- a/notes2web.py +++ b/notes2web.py @@ -116,6 +116,7 @@ def get_args(): parser.add_argument('-F', '--force', action="store_true", help="Generate new output html even if source file was modified before output html") parser.add_argument('--fuse', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/fuse.js')) parser.add_argument('--searchjs', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/search.js')) + parser.add_argument('--tocsearchjs', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/toc_search.js')) return parser.parse_args() @@ -345,6 +346,7 @@ def main(args): shutil.copyfile(args.stylesheet, args.output_dir.joinpath('styles.css')) shutil.copyfile(args.fuse, args.output_dir.joinpath('fuse.js')) shutil.copyfile(args.searchjs, args.output_dir.joinpath('search.js')) + shutil.copyfile(args.tocsearchjs, args.output_dir.joinpath('toc_search.js')) with open(args.output_dir.joinpath('index.html'), 'w+') as fp: with open(args.home_index) as fp2: html = re.sub(r'\$title\$', args.output_dir.parts[0], fp2.read()) diff --git a/styles.css b/styles.css index 0385df5..221e72d 100644 --- a/styles.css +++ b/styles.css @@ -25,7 +25,13 @@ body { display: flex } -#search { flex-grow: 9 } +#search { flex-grow: 9; } + +#sidebar #search { + flex-grow: 0; + padding: 1em; + margin: 0 1em 1em 1em; +} #results { overflow-x: scroll; @@ -63,6 +69,7 @@ body { display: flex; flex-direction: column; min-width: 20em; + padding-right: 1em; } #toc { diff --git a/templates/article.html b/templates/article.html index f6983a6..bad2f64 100644 --- a/templates/article.html +++ b/templates/article.html @@ -29,6 +29,7 @@

Table of Contents

+
$toc$
@@ -47,4 +48,6 @@ $body$
+ + diff --git a/toc_search.js b/toc_search.js new file mode 100644 index 0000000..4154c4e --- /dev/null +++ b/toc_search.js @@ -0,0 +1,94 @@ +'use strict'; + +var raw_html_tree = document.getElementById('toc').firstChild.cloneNode(true); + +function createSearchable(el) { + var par = el.parentElement; + var obj = el.cloneNode(true); + + while(par != raw_html_tree) { + var clone_parent = par.cloneNode(true); + + while (clone_parent.firstChild != clone_parent.lastChild) { + clone_parent.removeChild(clone_parent.lastChild); + } + console.log("obj.innerHTML: " + obj.innerHTML); + console.log("clone_parent.firstChild.innerHTML: " + clone_parent.firstChild.innerHTML); + if (obj.innerHTML != clone_parent.firstChild.innerHTML) + clone_parent.appendChild(obj); + + obj = clone_parent; + par = par.parentElement; + } + + return { + searchable: el.innerHTML, + obj: obj + }; +} + +var searchables = []; +Array(...raw_html_tree.getElementsByTagName('a')) + .forEach(el => searchables.push(createSearchable(el))); + +var fuse = new Fuse(searchables, { keys: [ 'searchable' ], includeMatches: true}); +var searchBar = document.getElementById('search'); +var resultsDiv = document.getElementById('toc'); + +function updateResults() { + var ul = document.createElement('ul'); + resultsDiv.innerHTML = ''; + if (searchBar.value == '') { + resultsDiv.appendChild(raw_html_tree); + return; + } + var results = fuse.search(searchBar.value); + + + results.forEach(r => { + console.log(r) + var content = r.item.obj + var last_a = Array.from(r.item.obj.getElementsByTagName('a')).pop() + + r.matches.reverse().every(match => { + var display_match = match.value; + if (match.indices.length >= 1) { + match.indices.sort((a, b) => (b[1]-b[0])-(a[1]-a[0])); + const indexPair = match.indices[0]; + const matching_slice = match.value.slice(indexPair[0], indexPair[1]+1); + last_a.innerHTML = match.value.replace( + matching_slice, + '' + matching_slice + '' + ); + } + return true; + }) + + ul.appendChild(content); + ul.appendChild(document.createElement('br')); + }) + resultsDiv.appendChild(ul); +} + +searchBar.addEventListener('keyup', e => { + // if user pressed enter + if (e.keyCode === 13) { + if (e.shiftKey) { + window.open(results[0].item.path, '_blank'); + } else { + window.location.href = results[0].item.path; + } + return; + } + updateResults(); +}) + +searchBar.addEventListener('change', updateResults); + +const searchParams = new URL(window.location.href).searchParams; +searchBar.value = searchParams.get('q'); +updateResults(); + +if (searchParams.has('lucky')) { + window.location.href = results[0].item.path; +}