neighbour-utils.js
Table of Contents
This literate configuration file tangles to the script
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 })();