neighbour-utils.js

This script is used by both neighbour-loader.js and mobile-menu.js . It consolidates the graph data layer and the neighbour list rendering into a single self-contained module. It fetches and caches /graph.json, queries neighbours for a given note, and returns a grouped DOM fragment via callback. It exposes a single public function: NeighbourUtils.buildList(lnk, listClass, onReady, onError).

Data Cache

Fetches /graph.json once and caches the result. Subsequent calls return the cached promise immediately without a network request.

var _cache = null;

function fetch_data() {
    if (_cache) return Promise.resolve(_cache);
    return fetch('/graph.json')
        .then(function (r) { return r.json(); })
        .then(function (data) { _cache = data; return data; });
}

Graph Queries

Pure query functions over raw data. findNode does a case-insensitive fallback lookup. buildDirectionMap classifies each neighbour edge as incoming, outgoing, or both. getNeighbours returns all connected nodes.

function findNode(data, lnk) {
    return data.nodes.find(function (n) { return n.lnk === lnk; }) ||
        data.nodes.find(function (n) { return n.lnk.toLowerCase() === lnk.toLowerCase(); });
}

function buildDirectionMap(data, currentNode) {
    const map = new Map();
    data.links.forEach(function (l) {
        if (l.source === currentNode.id) {
            const e = map.get(l.target) || { out: false, in: false };
            e.out = true; map.set(l.target, e);
        }
        if (l.target === currentNode.id) {
            const e = map.get(l.source) || { out: false, in: false };
            e.in = true; map.set(l.source, e);
        }
    });
    return map;
}

function getNeighbours(data, currentNode, map) {
    return data.nodes.filter(function (n) { return map.has(n.id); });
}

Build List

Fetches data, groups neighbours alphabetically into Links and Backlinks, builds a DOM fragment and returns it via onReady(count, fragment). onError is called if the node is not found or the fetch fails.

function buildList(lnk, listClass, onReady, onError) {
    fetch_data()
        .then(data => {
            const current = findNode(data, lnk);
            if (!current) { if (onError) onError(); return; }
            const map        = buildDirectionMap(data, current);
            const neighbours = getNeighbours(data, current, map);
            if (!neighbours.length) { if (onReady) onReady(0, null); return; }
            const fragment  = document.createDocumentFragment();
            const links     = neighbours.filter(nb => map.get(nb.id).out)
                                        .sort((a, b) => a.label.localeCompare(b.label));
            const backlinks = neighbours.filter(nb => map.get(nb.id).in)
                                        .sort((a, b) => a.label.localeCompare(b.label));
            [['Links', links], ['Backlinks', backlinks]].forEach(([heading, group]) => {
                if (!group.length) return;
                const h = document.createElement('div');
                h.className = 'np-group-heading';
                h.textContent = heading;
                fragment.appendChild(h);
                const ul = document.createElement('ul');
                ul.className = listClass;
                group.forEach(nb => {
                    const li = document.createElement('li');
                    const a  = document.createElement('a');
                    a.href        = `/docs/${nb.lnk}/`;
                    a.textContent = nb.label;
                    li.appendChild(a);
                    ul.appendChild(li);
                });
                fragment.appendChild(ul);
            });
            if (onReady) onReady(neighbours.length, fragment);
        })
        .catch(() => { if (onError) onError(); });
}

Assembly

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

(function () {
    // data-cache → graph-queries → build-list → expose API
})();

© Prabu Anand K 2020-2026