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