search.js

search.js provides the search functionality for the graphical view at /map/. It fetches /index.json (Hugo’s search index) and wires up the #elbi-input field. It integrates with graph.js via the globals window.nodeByLink, window.showNode, window.focusByTerm, window.centreOnGraph, window.resetGraph, window.getCurrentNode, and window.getNodeLnk.

Since graph.js is loaded asynchronously, initSearch polls until those globals are available, retrying up to 50 times at 100ms intervals.

Initialisation Guard

Polls for graph.js globals before proceeding. Gives up after 5 seconds (50 × 100ms) to avoid infinite polling on error.

let _retries = 0;
function initSearch() {
    if (!window.nodeByLink || !window.showNode) {
        if (++_retries > 50) return;
        setTimeout(initSearch, 100);
        return;
    }
    const input    = document.getElementById("elbi-input");
    const results  = document.getElementById("elbi-results");
    const exitLink = document.getElementById("graph-exit-link");
    if (exitLink) {
        exitLink.addEventListener("click", (e) => {
            e.preventDefault();
            const node = window.getCurrentNode?.();
            window.location.href = node ? "/docs/" + window.getNodeLnk(node) + "/" : "/";
        });
    }
    fetch("/index.json")
        .then(r => r.json())
        .then(pages => {
            input.addEventListener("input", function () {
                results.innerHTML = "";
                window.focusByTerm?.(input.value.trim());
                if (!input.value.trim()) {
                    const activeNode = window.getCurrentNode?.();
                    if (activeNode) window.showNode(activeNode);
                    return;
                }
                window.centreOnGraph?.();
                const terms = input.value
                    .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
                    .toLowerCase().split(" ").filter(Boolean);

                let filtered = pages.filter(page =>
                    terms.every(term =>
                        JSON.stringify(page)
                            .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
                            .toLowerCase().includes(term)
                    )
                );

                filtered.forEach(p => {
                    p.score = 0;
                    terms.forEach(term => {
                        if (p.title?.toLowerCase().includes(term))   p.score += 3;
                        if (p.summary?.toLowerCase().includes(term)) p.score += 1;
                    });
                });

                filtered = filtered
                    .filter(p => p.score > 0)
                    .sort((a, b) => b.score - a.score);

                filtered.forEach(page => {
                    const lnk    = page.relpermalink.replace(/^\/docs\//, "").replace(/\/$/, "");
                    const nodeId = window.nodeByLink[lnk];
                    if (!nodeId) return;
                    results.insertAdjacentHTML("beforeend",
                        `<li><a href="#" data-node="${nodeId}">${page.title}</a></li>`);
                });
            });
            input.addEventListener("keyup", (e) => {
                if (e.key !== "Escape") return;
                e.stopPropagation();
                input.value = "";
                results.innerHTML = "";
                input.blur();
            });
            results.addEventListener("click", (e) => {
                const a = e.target.closest("a[data-node]");
                if (!a) return;
                e.preventDefault();
                window.resetGraph?.();
                window.showNode(a.getAttribute("data-node"));
                input.value = "";
                results.innerHTML = "";
            });
        })
        .catch(e => {
            if (e.name !== 'AbortError') console.warn("index.json fetch failed", e);
        });
}
initSearch();

Handles the “Text View” link on the graph page. Navigates to the currently focused note’s doc page if one is active, otherwise to the site root.

const input    = document.getElementById("elbi-input");
const results  = document.getElementById("elbi-results");
const exitLink = document.getElementById("graph-exit-link");
if (exitLink) {
    exitLink.addEventListener("click", (e) => {
        e.preventDefault();
        const node = window.getCurrentNode?.();
        window.location.href = node ? "/docs/" + window.getNodeLnk(node) + "/" : "/";
    });
}

Search Index

Fetches /index.json once, then wires up input, keyup, and results click listeners. Scoring weights title matches (3 points) above summary matches (1 point). Diacritics are normalised before matching so accented characters match their base form.

fetch("/index.json")
    .then(r => r.json())
    .then(pages => {
        input.addEventListener("input", function () {
            results.innerHTML = "";
            window.focusByTerm?.(input.value.trim());
            if (!input.value.trim()) {
                const activeNode = window.getCurrentNode?.();
                if (activeNode) window.showNode(activeNode);
                return;
            }
            window.centreOnGraph?.();
            const terms = input.value
                .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
                .toLowerCase().split(" ").filter(Boolean);

            let filtered = pages.filter(page =>
                terms.every(term =>
                    JSON.stringify(page)
                        .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
                        .toLowerCase().includes(term)
                )
            );

            filtered.forEach(p => {
                p.score = 0;
                terms.forEach(term => {
                    if (p.title?.toLowerCase().includes(term))   p.score += 3;
                    if (p.summary?.toLowerCase().includes(term)) p.score += 1;
                });
            });

            filtered = filtered
                .filter(p => p.score > 0)
                .sort((a, b) => b.score - a.score);

            filtered.forEach(page => {
                const lnk    = page.relpermalink.replace(/^\/docs\//, "").replace(/\/$/, "");
                const nodeId = window.nodeByLink[lnk];
                if (!nodeId) return;
                results.insertAdjacentHTML("beforeend",
                    `<li><a href="#" data-node="${nodeId}">${page.title}</a></li>`);
            });
        });
        input.addEventListener("keyup", (e) => {
            if (e.key !== "Escape") return;
            e.stopPropagation();
            input.value = "";
            results.innerHTML = "";
            input.blur();
        });
        results.addEventListener("click", (e) => {
            const a = e.target.closest("a[data-node]");
            if (!a) return;
            e.preventDefault();
            window.resetGraph?.();
            window.showNode(a.getAttribute("data-node"));
            input.value = "";
            results.innerHTML = "";
        });
    })
    .catch(e => {
        if (e.name !== 'AbortError') console.warn("index.json fetch failed", e);
    });

Input Listener

On each keystroke, normalises the query, filters pages, scores and sorts results, then renders matching titles as clickable list items. Also syncs the graph highlight via focusByTerm.

input.addEventListener("input", function () {
    results.innerHTML = "";
    window.focusByTerm?.(input.value.trim());
    if (!input.value.trim()) {
        const activeNode = window.getCurrentNode?.();
        if (activeNode) window.showNode(activeNode);
        return;
    }
    window.centreOnGraph?.();
    const terms = input.value
        .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
        .toLowerCase().split(" ").filter(Boolean);

    let filtered = pages.filter(page =>
        terms.every(term =>
            JSON.stringify(page)
                .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
                .toLowerCase().includes(term)
        )
    );

    filtered.forEach(p => {
        p.score = 0;
        terms.forEach(term => {
            if (p.title?.toLowerCase().includes(term))   p.score += 3;
            if (p.summary?.toLowerCase().includes(term)) p.score += 1;
        });
    });

    filtered = filtered
        .filter(p => p.score > 0)
        .sort((a, b) => b.score - a.score);

    filtered.forEach(page => {
        const lnk    = page.relpermalink.replace(/^\/docs\//, "").replace(/\/$/, "");
        const nodeId = window.nodeByLink[lnk];
        if (!nodeId) return;
        results.insertAdjacentHTML("beforeend",
            `<li><a href="#" data-node="${nodeId}">${page.title}</a></li>`);
    });
});

Keyup Listener

Clears input and results on Escape.

input.addEventListener("keyup", (e) => {
    if (e.key !== "Escape") return;
    e.stopPropagation();
    input.value = "";
    results.innerHTML = "";
    input.blur();
});

Results Listener

Handles clicks on search result links. Navigates the graph to the selected node and clears the search state.

results.addEventListener("click", (e) => {
    const a = e.target.closest("a[data-node]");
    if (!a) return;
    e.preventDefault();
    window.resetGraph?.();
    window.showNode(a.getAttribute("data-node"));
    input.value = "";
    results.innerHTML = "";
});

Assembly

initSearch uses nested noweb — exit-link and search-index are referenced inside init-guard, and the three listeners inside search-index. This reflects the runtime structure: all code runs only after initSearch confirms the graph.js globals are present.

Nested noweb references work in org-babel provided `:noweb yes` is declared on every block in the chain that contains references, not just the final Assembly block. See the Org Mode manual: Noweb Reference Syntax↗ for details.

The complete tangled version of this file can be found here↗ .

(function () {
    // init-guard
    //   └── exit-link
    //   └── search-index
    //         └── input-listener
    //         └── keyup-listener
    //         └── results-listener
})();

© Prabu Anand K 2020-2026